package publish import ( "context" "fmt" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/state" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/transform" ) type Action string const ( ActionPublishNew Action = "publish_new" ActionReplaceOlder Action = "replace_older" ActionSkipSame Action = "skip_same" ActionSkipDestinationNewer Action = "skip_destination_newer" ActionFailConflict Action = "fail_conflict" ActionFailUnmanaged Action = "fail_unmanaged" ActionForceReplace Action = "force_replace" ) type Request struct { PipelineID string DestinationID string SourceBundle bundle.Bundle SourceBackend storage.Backend DestinationBackend storage.Backend DestinationBundlePath string Publish config.PublishPolicy Transform config.Transform Transformers TransformerResolver Transfer config.TransferPolicy DistributorVersion string Force bool } type TransformerResolver interface { Get(name string) (transform.Transformer, bool) } type Plan struct { PipelineID string DestinationID string BundleID string BundlePath string DestinationBundlePath string Action Action Reason string Force bool Outputs []Output ExistingState *state.DistributorState } type Output struct { SourcePath string DestinationPath string Kind string Transform string Data []byte SHA256 string Size int64 } func Build(ctx context.Context, req Request) (Plan, error) { if err := validateRequest(req); err != nil { return Plan{}, err } outputs, err := PlanOutputs(ctx, req) if err != nil { return Plan{}, err } status, err := inspectDestination(ctx, req.DestinationBackend, req.DestinationBundlePath) if err != nil { return Plan{}, err } comparison := state.Compare(req.SourceBundle.Manifest, req.PipelineID, req.DestinationID, status) action, reason := actionForComparison(comparison, req.Transfer, req.Force) plan := Plan{ PipelineID: req.PipelineID, DestinationID: req.DestinationID, BundleID: req.SourceBundle.Manifest.ID, BundlePath: req.SourceBundle.RootRelativePath, DestinationBundlePath: req.DestinationBundlePath, Action: action, Reason: reason, Force: action == ActionForceReplace, Outputs: outputs, ExistingState: status.State, } if action == ActionFailConflict || action == ActionFailUnmanaged { return plan, fmt.Errorf("%s: %s", action, reason) } return plan, nil } func validateRequest(req Request) error { if req.PipelineID == "" { return fmt.Errorf("pipeline id is required") } if req.DestinationID == "" { return fmt.Errorf("destination id is required") } if req.SourceBackend == nil { return fmt.Errorf("source backend is required") } if req.DestinationBackend == nil { return fmt.Errorf("destination backend is required") } if err := config.ValidatePublishTransformPolicy(req.Publish, req.Transform); err != nil { return fmt.Errorf("publish/transform policy: %w", err) } return nil } func actionForComparison(comparison state.Comparison, transfer config.TransferPolicy, force bool) (Action, string) { switch comparison.Outcome { case state.OutcomeDestinationAbsent: return ActionPublishNew, comparison.Reason case state.OutcomeDestinationUnmanaged: if force { return ActionForceReplace, "forced replacement of unmanaged destination content" } return ActionFailUnmanaged, comparison.Reason case state.OutcomeInvalidState: return ActionFailConflict, comparison.Reason case state.OutcomeIdentityMismatch, state.OutcomeSameCreatedConflict, state.OutcomeDifferentSourceConflict: if transfer.OnConflict == config.TransferActionReplace { if force { return ActionForceReplace, "forced replacement of conflicting destination state: " + comparison.Reason } return ActionFailConflict, "destination conflict replacement requires --force" } return ActionFailConflict, comparison.Reason case state.OutcomeSameSource: if transfer.OnDestinationSame == config.TransferActionFail { return ActionFailConflict, "destination matches source and transfer policy requires failure" } return ActionSkipSame, comparison.Reason case state.OutcomeDestinationOlder: if transfer.OnDestinationOlder == config.TransferActionFail { return ActionFailConflict, "destination is older and transfer policy requires failure" } return ActionReplaceOlder, comparison.Reason case state.OutcomeDestinationNewer: if transfer.OnDestinationNewer == config.TransferActionReplace { if force { return ActionForceReplace, "forced replacement of newer destination state" } return ActionFailConflict, "destination is newer and replacement requires --force" } if transfer.OnDestinationNewer == config.TransferActionFail { return ActionFailConflict, "destination is newer and transfer policy requires failure" } return ActionSkipDestinationNewer, comparison.Reason default: return ActionFailConflict, "unsupported comparison outcome" } }