package state import ( "fmt" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/link" "gitea.maximumdirect.net/eric/distributor/internal/storage" ) const ( OutputKindSource = "source" OutputKindGenerated = "generated" ) func Validate(s DistributorState) error { if s.SchemaVersion != SchemaVersion { return fmt.Errorf("state schema_version must be %d", SchemaVersion) } if s.PipelineID == "" { return fmt.Errorf("state pipeline_id is required") } if s.DestinationID == "" { return fmt.Errorf("state destination_id is required") } if s.PublishedAt.IsZero() { return fmt.Errorf("state published_at is required") } if s.CreatedAt.IsZero() { return fmt.Errorf("state created_at is required") } if s.UpdatedAt.IsZero() { return fmt.Errorf("state updated_at is required") } if s.State.Mode != StateModeSingleOwner { return fmt.Errorf("state state.mode must be %s", StateModeSingleOwner) } if s.Reconciliation.Mode != config.ReconciliationModeReplace && s.Reconciliation.Mode != config.ReconciliationModeMerge { return fmt.Errorf("state reconciliation.mode must be %s or %s", config.ReconciliationModeReplace, config.ReconciliationModeMerge) } if err := validateEmbeddedManifest(s.Source.Manifest); err != nil { return fmt.Errorf("state source.manifest: %w", err) } if s.Links != nil && s.Links.PrimaryURL != "" { if err := link.ValidateHTTPURL(s.Links.PrimaryURL); err != nil { return fmt.Errorf("state links.primary_url: %w", err) } } if s.Outputs == nil { return fmt.Errorf("state outputs is required") } seen := make(map[string]struct{}, len(s.Outputs)) for index, output := range s.Outputs { if err := validateOutput(index, output); err != nil { return err } if _, exists := seen[output.Path]; exists { return fmt.Errorf("state outputs[%d].path duplicates %q", index, output.Path) } seen[output.Path] = struct{}{} } return nil } func validateEmbeddedManifest(manifest bundle.Manifest) error { return bundle.ValidateManifest(manifest) } func validateOutput(index int, output OutputFile) error { if err := storage.ValidatePath(output.Path); err != nil { return fmt.Errorf("state outputs[%d].path: %w", index, err) } switch output.Kind { case OutputKindSource, OutputKindGenerated: default: return fmt.Errorf("state outputs[%d].kind must be source or generated", index) } if err := storage.ValidatePath(output.SourcePath); err != nil { return fmt.Errorf("state outputs[%d].source_path: %w", index, err) } if output.Kind == OutputKindGenerated && output.Transform == "" { return fmt.Errorf("state outputs[%d].transform is required for generated output", index) } if output.URL != "" { if err := link.ValidateHTTPURL(output.URL); err != nil { return fmt.Errorf("state outputs[%d].url: %w", index, err) } } if err := bundle.ValidateDigest(output.SHA256); err != nil { return fmt.Errorf("state outputs[%d].sha256: %w", index, err) } if output.Size < 0 { return fmt.Errorf("state outputs[%d].size must be non-negative", index) } if output.CreatedAt.IsZero() { return fmt.Errorf("state outputs[%d].created_at is required", index) } if output.UpdatedAt.IsZero() { return fmt.Errorf("state outputs[%d].updated_at is required", index) } return nil }