Add reconciliation state foundation
This commit is contained in:
@@ -8,9 +8,14 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||
)
|
||||
|
||||
const SchemaVersion = 1
|
||||
const (
|
||||
SchemaVersion = 2
|
||||
legacySchemaVersion = 1
|
||||
StateModeSingleOwner = "single_owner"
|
||||
)
|
||||
|
||||
type DistributorState struct {
|
||||
SchemaVersion int
|
||||
@@ -18,11 +23,23 @@ type DistributorState struct {
|
||||
PipelineID string
|
||||
DestinationID string
|
||||
PublishedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
State StatePolicy
|
||||
Reconciliation ReconciliationPolicy
|
||||
Source SourceState
|
||||
Links *LinkState
|
||||
Outputs []OutputFile
|
||||
}
|
||||
|
||||
type StatePolicy struct {
|
||||
Mode string
|
||||
}
|
||||
|
||||
type ReconciliationPolicy struct {
|
||||
Mode string
|
||||
}
|
||||
|
||||
type SourceState struct {
|
||||
Manifest bundle.Manifest
|
||||
}
|
||||
@@ -39,17 +56,31 @@ type OutputFile struct {
|
||||
URL string
|
||||
SHA256 string
|
||||
Size int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type rawDistributorState struct {
|
||||
SchemaVersion *int `json:"schema_version"`
|
||||
DistributorVersion string `json:"distributor_version"`
|
||||
PipelineID *string `json:"pipeline_id"`
|
||||
DestinationID *string `json:"destination_id"`
|
||||
PublishedAt *string `json:"published_at"`
|
||||
Source *rawSourceState `json:"source"`
|
||||
Links *rawLinkState `json:"links"`
|
||||
Outputs []rawOutputFile `json:"outputs"`
|
||||
SchemaVersion *int `json:"schema_version"`
|
||||
DistributorVersion string `json:"distributor_version"`
|
||||
PipelineID *string `json:"pipeline_id"`
|
||||
DestinationID *string `json:"destination_id"`
|
||||
PublishedAt *string `json:"published_at"`
|
||||
CreatedAt *string `json:"created_at"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
State *rawStatePolicy `json:"state"`
|
||||
Reconciliation *rawReconciliationPolicy `json:"reconciliation"`
|
||||
Source *rawSourceState `json:"source"`
|
||||
Links *rawLinkState `json:"links"`
|
||||
Outputs []rawOutputFile `json:"outputs"`
|
||||
}
|
||||
|
||||
type rawStatePolicy struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type rawReconciliationPolicy struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type rawSourceState struct {
|
||||
@@ -68,6 +99,8 @@ type rawOutputFile struct {
|
||||
URL string `json:"url"`
|
||||
SHA256 *string `json:"sha256"`
|
||||
Size *int64 `json:"size"`
|
||||
CreatedAt *string `json:"created_at"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func Parse(data []byte) (DistributorState, error) {
|
||||
@@ -96,9 +129,10 @@ func parseRaw(raw rawDistributorState) (DistributorState, error) {
|
||||
return DistributorState{}, fmt.Errorf("state schema_version is required")
|
||||
}
|
||||
state.SchemaVersion = *raw.SchemaVersion
|
||||
if state.SchemaVersion != SchemaVersion {
|
||||
return DistributorState{}, fmt.Errorf("state schema_version must be %d", SchemaVersion)
|
||||
if state.SchemaVersion != SchemaVersion && state.SchemaVersion != legacySchemaVersion {
|
||||
return DistributorState{}, fmt.Errorf("state schema_version must be %d or %d", legacySchemaVersion, SchemaVersion)
|
||||
}
|
||||
legacy := state.SchemaVersion == legacySchemaVersion
|
||||
state.DistributorVersion = raw.DistributorVersion
|
||||
if raw.PipelineID == nil || *raw.PipelineID == "" {
|
||||
return DistributorState{}, fmt.Errorf("state pipeline_id is required")
|
||||
@@ -116,6 +150,32 @@ func parseRaw(raw rawDistributorState) (DistributorState, error) {
|
||||
return DistributorState{}, fmt.Errorf("state published_at must be RFC3339: %w", err)
|
||||
}
|
||||
state.PublishedAt = publishedAt.UTC()
|
||||
if legacy {
|
||||
state.SchemaVersion = SchemaVersion
|
||||
state.CreatedAt = state.PublishedAt
|
||||
state.UpdatedAt = state.PublishedAt
|
||||
state.State.Mode = StateModeSingleOwner
|
||||
state.Reconciliation.Mode = config.ReconciliationModeReplace
|
||||
} else {
|
||||
createdAt, err := parseRequiredTime("state created_at", raw.CreatedAt)
|
||||
if err != nil {
|
||||
return DistributorState{}, err
|
||||
}
|
||||
updatedAt, err := parseRequiredTime("state updated_at", raw.UpdatedAt)
|
||||
if err != nil {
|
||||
return DistributorState{}, err
|
||||
}
|
||||
state.CreatedAt = createdAt
|
||||
state.UpdatedAt = updatedAt
|
||||
if raw.State == nil || raw.State.Mode == "" {
|
||||
return DistributorState{}, fmt.Errorf("state state.mode is required")
|
||||
}
|
||||
state.State.Mode = raw.State.Mode
|
||||
if raw.Reconciliation == nil || raw.Reconciliation.Mode == "" {
|
||||
return DistributorState{}, fmt.Errorf("state reconciliation.mode is required")
|
||||
}
|
||||
state.Reconciliation.Mode = raw.Reconciliation.Mode
|
||||
}
|
||||
if raw.Source == nil || len(raw.Source.Manifest) == 0 {
|
||||
return DistributorState{}, fmt.Errorf("state source.manifest is required")
|
||||
}
|
||||
@@ -130,7 +190,7 @@ func parseRaw(raw rawDistributorState) (DistributorState, error) {
|
||||
if raw.Outputs == nil {
|
||||
return DistributorState{}, fmt.Errorf("state outputs is required")
|
||||
}
|
||||
outputs, err := parseOutputs(raw.Outputs)
|
||||
outputs, err := parseOutputs(raw.Outputs, legacy, state.PublishedAt)
|
||||
if err != nil {
|
||||
return DistributorState{}, err
|
||||
}
|
||||
@@ -138,11 +198,22 @@ func parseRaw(raw rawDistributorState) (DistributorState, error) {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func parseOutputs(rawOutputs []rawOutputFile) ([]OutputFile, error) {
|
||||
func parseRequiredTime(context string, raw *string) (time.Time, error) {
|
||||
if raw == nil || *raw == "" {
|
||||
return time.Time{}, fmt.Errorf("%s is required", context)
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, *raw)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("%s must be RFC3339: %w", context, err)
|
||||
}
|
||||
return parsed.UTC(), nil
|
||||
}
|
||||
|
||||
func parseOutputs(rawOutputs []rawOutputFile, legacy bool, publishedAt time.Time) ([]OutputFile, error) {
|
||||
outputs := make([]OutputFile, 0, len(rawOutputs))
|
||||
seen := make(map[string]struct{}, len(rawOutputs))
|
||||
for index, raw := range rawOutputs {
|
||||
output, err := parseOutput(index, raw)
|
||||
output, err := parseOutput(index, raw, legacy, publishedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -155,7 +226,7 @@ func parseOutputs(rawOutputs []rawOutputFile) ([]OutputFile, error) {
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func parseOutput(index int, raw rawOutputFile) (OutputFile, error) {
|
||||
func parseOutput(index int, raw rawOutputFile, legacy bool, publishedAt time.Time) (OutputFile, error) {
|
||||
if raw.Path == nil || *raw.Path == "" {
|
||||
return OutputFile{}, fmt.Errorf("state outputs[%d].path is required", index)
|
||||
}
|
||||
@@ -171,6 +242,19 @@ func parseOutput(index int, raw rawOutputFile) (OutputFile, error) {
|
||||
if raw.Size == nil {
|
||||
return OutputFile{}, fmt.Errorf("state outputs[%d].size is required", index)
|
||||
}
|
||||
createdAt := publishedAt
|
||||
updatedAt := publishedAt
|
||||
if !legacy {
|
||||
var err error
|
||||
createdAt, err = parseRequiredTime(fmt.Sprintf("state outputs[%d].created_at", index), raw.CreatedAt)
|
||||
if err != nil {
|
||||
return OutputFile{}, err
|
||||
}
|
||||
updatedAt, err = parseRequiredTime(fmt.Sprintf("state outputs[%d].updated_at", index), raw.UpdatedAt)
|
||||
if err != nil {
|
||||
return OutputFile{}, err
|
||||
}
|
||||
}
|
||||
return OutputFile{
|
||||
Path: *raw.Path,
|
||||
Kind: *raw.Kind,
|
||||
@@ -179,6 +263,8 @@ func parseOutput(index int, raw rawOutputFile) (OutputFile, error) {
|
||||
URL: raw.URL,
|
||||
SHA256: *raw.SHA256,
|
||||
Size: *raw.Size,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -186,19 +272,39 @@ func (s DistributorState) PublishedAtString() string {
|
||||
return s.PublishedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (s DistributorState) CreatedAtString() string {
|
||||
return s.CreatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (s DistributorState) UpdatedAtString() string {
|
||||
return s.UpdatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o OutputFile) CreatedAtString() string {
|
||||
return o.CreatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o OutputFile) UpdatedAtString() string {
|
||||
return o.UpdatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (s DistributorState) MarshalJSON() ([]byte, error) {
|
||||
type sourceJSON struct {
|
||||
Manifest bundle.Manifest `json:"manifest"`
|
||||
}
|
||||
type stateJSON struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
DistributorVersion string `json:"distributor_version,omitempty"`
|
||||
PipelineID string `json:"pipeline_id"`
|
||||
DestinationID string `json:"destination_id"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Source sourceJSON `json:"source"`
|
||||
Links *LinkState `json:"links,omitempty"`
|
||||
Outputs []OutputFile `json:"outputs"`
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
DistributorVersion string `json:"distributor_version,omitempty"`
|
||||
PipelineID string `json:"pipeline_id"`
|
||||
DestinationID string `json:"destination_id"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
State StatePolicy `json:"state"`
|
||||
Reconciliation ReconciliationPolicy `json:"reconciliation"`
|
||||
Source sourceJSON `json:"source"`
|
||||
Links *LinkState `json:"links,omitempty"`
|
||||
Outputs []OutputFile `json:"outputs"`
|
||||
}
|
||||
return json.Marshal(stateJSON{
|
||||
SchemaVersion: s.SchemaVersion,
|
||||
@@ -206,12 +312,30 @@ func (s DistributorState) MarshalJSON() ([]byte, error) {
|
||||
PipelineID: s.PipelineID,
|
||||
DestinationID: s.DestinationID,
|
||||
PublishedAt: s.PublishedAtString(),
|
||||
CreatedAt: s.CreatedAtString(),
|
||||
UpdatedAt: s.UpdatedAtString(),
|
||||
State: s.State,
|
||||
Reconciliation: s.Reconciliation,
|
||||
Source: sourceJSON{Manifest: s.Source.Manifest},
|
||||
Links: s.Links,
|
||||
Outputs: s.Outputs,
|
||||
})
|
||||
}
|
||||
|
||||
func (p StatePolicy) MarshalJSON() ([]byte, error) {
|
||||
type policyJSON struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
return json.Marshal(policyJSON{Mode: p.Mode})
|
||||
}
|
||||
|
||||
func (p ReconciliationPolicy) MarshalJSON() ([]byte, error) {
|
||||
type policyJSON struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
return json.Marshal(policyJSON{Mode: p.Mode})
|
||||
}
|
||||
|
||||
func (l LinkState) MarshalJSON() ([]byte, error) {
|
||||
type linkJSON struct {
|
||||
PrimaryURL string `json:"primary_url,omitempty"`
|
||||
@@ -228,6 +352,8 @@ func (o OutputFile) MarshalJSON() ([]byte, error) {
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
return json.Marshal(outputJSON{
|
||||
Path: o.Path,
|
||||
@@ -237,5 +363,7 @@ func (o OutputFile) MarshalJSON() ([]byte, error) {
|
||||
URL: o.URL,
|
||||
SHA256: o.SHA256,
|
||||
Size: o.Size,
|
||||
CreatedAt: o.CreatedAt.UTC().Format(time.RFC3339),
|
||||
UpdatedAt: o.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user