189 lines
6.5 KiB
Go
189 lines
6.5 KiB
Go
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
|
|
PathMapping string
|
|
Publish config.PublishPolicy
|
|
Transform config.Transform
|
|
Links *config.Links
|
|
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
|
|
PathMapping string
|
|
Action Action
|
|
Reason string
|
|
Force bool
|
|
PrimaryURL string
|
|
Outputs []Output
|
|
ExistingState *state.DistributorState
|
|
}
|
|
|
|
type Output struct {
|
|
SourcePath string
|
|
DestinationPath string
|
|
Kind string
|
|
Transform string
|
|
URL 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
|
|
}
|
|
outputs, primaryURL, err := PlanLinks(req, outputs)
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
status, err := inspectDestination(ctx, req.DestinationBackend, req.DestinationBundlePath)
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
comparison := compareDestination(req, 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,
|
|
PathMapping: req.PathMapping,
|
|
Action: action,
|
|
Reason: reason,
|
|
Force: action == ActionForceReplace,
|
|
PrimaryURL: primaryURL,
|
|
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 compareDestination(req Request, status state.DestinationStatus) state.Comparison {
|
|
comparison := state.Compare(req.SourceBundle.Manifest, req.PipelineID, req.DestinationID, status)
|
|
if req.PathMapping != config.PathMappingFixed || comparison.Outcome != state.OutcomeDifferentSourceConflict || status.State == nil {
|
|
return comparison
|
|
}
|
|
destinationManifest := status.State.Source.Manifest
|
|
if destinationManifest.Created.Before(req.SourceBundle.Manifest.Created) {
|
|
return state.Comparison{Outcome: state.OutcomeDestinationOlder, Reason: "fixed destination source is older than selected source"}
|
|
}
|
|
if destinationManifest.Created.After(req.SourceBundle.Manifest.Created) {
|
|
return state.Comparison{Outcome: state.OutcomeDestinationNewer, Reason: "fixed destination source is newer than selected source"}
|
|
}
|
|
return comparison
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|