From 09ba647570beaf838895b19ab8a20c9e8909b8ff Mon Sep 17 00:00:00 2001 From: Hideaki Murakami Date: Mon, 1 Dec 2025 11:01:21 +1300 Subject: [PATCH 1/2] Add support for runbook retention strategy --- pkg/runbooks/runbook.go | 4 +- pkg/runbooks/runbook_retention_period.go | 13 -- pkg/runbooks/runbook_retention_policy.go | 110 ++++++++++++ pkg/runbooks/runbook_retention_policy_test.go | 160 ++++++++++++++++++ 4 files changed, 272 insertions(+), 15 deletions(-) delete mode 100644 pkg/runbooks/runbook_retention_period.go create mode 100644 pkg/runbooks/runbook_retention_policy.go create mode 100644 pkg/runbooks/runbook_retention_policy_test.go diff --git a/pkg/runbooks/runbook.go b/pkg/runbooks/runbook.go index 513d73c2..2ddfd0d1 100644 --- a/pkg/runbooks/runbook.go +++ b/pkg/runbooks/runbook.go @@ -16,7 +16,7 @@ type Runbook struct { Name string `json:"Name,omitempty"` ProjectID string `json:"ProjectId,omitempty"` PublishedRunbookSnapshotID string `json:"PublishedRunbookSnapshotId,omitempty"` - RunRetentionPolicy *RunbookRetentionPeriod `json:"RunRetentionPolicy,omitempty"` + RunRetentionPolicy *RunbookRetentionPolicy `json:"RunRetentionPolicy,omitempty"` RunbookProcessID string `json:"RunbookProcessId,omitempty"` SpaceID string `json:"SpaceId,omitempty"` ForcePackageDownload bool `json:"ForcePackageDownload"` @@ -32,7 +32,7 @@ func NewRunbook(name string, projectID string) *Runbook { MultiTenancyMode: core.TenantedDeploymentModeUntenanted, Name: name, ProjectID: projectID, - RunRetentionPolicy: NewRunbookRetentionPeriod(), + RunRetentionPolicy: NewDefaultRunbookRetentionPolicy(), Resource: *resources.NewResource(), } } diff --git a/pkg/runbooks/runbook_retention_period.go b/pkg/runbooks/runbook_retention_period.go deleted file mode 100644 index d6a7cc2b..00000000 --- a/pkg/runbooks/runbook_retention_period.go +++ /dev/null @@ -1,13 +0,0 @@ -package runbooks - -type RunbookRetentionPeriod struct { - QuantityToKeep int32 `json:"QuantityToKeep"` - ShouldKeepForever bool `json:"ShouldKeepForever"` -} - -func NewRunbookRetentionPeriod() *RunbookRetentionPeriod { - return &RunbookRetentionPeriod{ - QuantityToKeep: 100, - ShouldKeepForever: false, - } -} diff --git a/pkg/runbooks/runbook_retention_policy.go b/pkg/runbooks/runbook_retention_policy.go new file mode 100644 index 00000000..f3737234 --- /dev/null +++ b/pkg/runbooks/runbook_retention_policy.go @@ -0,0 +1,110 @@ +package runbooks + +import ( + "encoding/json" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" +) + +type RunbookRetentionPolicy struct { + Strategy string `json:"Strategy"` + QuantityToKeep int32 `json:"QuantityToKeep"` + Unit string `json:"Unit,omitempty"` +} + +const ( + RunbookRetentionStrategyDefault string = "Default" + RunbookRetentionStrategyForever string = "Forever" + RunbookRetentionStrategyCount string = "Count" +) + +const ( + RunbookRetentionUnitDays string = "Days" + RunbookRetentionUnitItems string = "Items" +) + +func NewDefaultRunbookRetentionPolicy() *RunbookRetentionPolicy { + return &RunbookRetentionPolicy{ + Strategy: RunbookRetentionStrategyDefault, + QuantityToKeep: 100, + Unit: RunbookRetentionUnitItems, + } +} + +func NewCountBasedRunbookRetentionPolicy(quantityToKeep int32, unit string) (*RunbookRetentionPolicy, error) { + if quantityToKeep < 1 { + return nil, internal.CreateInvalidParameterError("NewCountBasedRunbookRetentionPolicy", "quantityToKeep") + } + + if unit != RunbookRetentionUnitDays && unit != RunbookRetentionUnitItems { + return nil, internal.CreateInvalidParameterError("NewCountBasedRunbookRetentionPolicy", "unit") + } + + return &RunbookRetentionPolicy{ + Strategy: RunbookRetentionStrategyCount, + QuantityToKeep: quantityToKeep, + Unit: unit, + }, nil +} + +func NewKeepForeverRunbookRetentionPolicy() *RunbookRetentionPolicy { + return &RunbookRetentionPolicy{ + Strategy: RunbookRetentionStrategyForever, + QuantityToKeep: 0, + Unit: RunbookRetentionUnitItems, + } +} + +// MarshalJSON to handle backward compatibility with older server versions +func (r *RunbookRetentionPolicy) MarshalJSON() ([]byte, error) { + var fields struct { + QuantityToKeep int32 `json:"QuantityToKeep"` + ShouldKeepForever bool `json:"ShouldKeepForever"` + Unit string `json:"Unit"` + Strategy string `json:"Strategy,omitempty"` + } + + fields.QuantityToKeep = r.QuantityToKeep + fields.Unit = r.Unit + fields.Strategy = r.Strategy + fields.ShouldKeepForever = r.Strategy == RunbookRetentionStrategyForever + + return json.Marshal(fields) +} + +// MarshalJSON to handle backward compatibility with older server versions +func (r *RunbookRetentionPolicy) UnmarshalJSON(data []byte) error { + var fields struct { + QuantityToKeep int32 `json:"QuantityToKeep"` + ShouldKeepForever bool `json:"ShouldKeepForever"` + Unit string `json:"Unit"` + Strategy string `json:"Strategy,omitempty"` + } + + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + r.QuantityToKeep = fields.QuantityToKeep + r.Unit = fields.Unit + + // If the Strategy field is present, use it directly + if fields.Strategy != "" { + r.Strategy = fields.Strategy + return nil + } + + // Infer the Strategy based on other fields for backward compatibility + if fields.QuantityToKeep == 0 || fields.ShouldKeepForever == true { + r.Strategy = RunbookRetentionStrategyForever + return nil + } + + if fields.QuantityToKeep == 100 && r.Unit == RunbookRetentionUnitItems { + r.Strategy = RunbookRetentionStrategyDefault + return nil + } + + r.Strategy = RunbookRetentionStrategyCount + return nil +} diff --git a/pkg/runbooks/runbook_retention_policy_test.go b/pkg/runbooks/runbook_retention_policy_test.go new file mode 100644 index 00000000..cb1ff52b --- /dev/null +++ b/pkg/runbooks/runbook_retention_policy_test.go @@ -0,0 +1,160 @@ +package runbooks + +import ( + "encoding/json" + "testing" + + "github.com/kinbiko/jsonassert" + "github.com/stretchr/testify/require" +) + +func TestCountBasedRunbookRetentionPolicyMarshalJSON(t *testing.T) { + expectedJson := `{ + "Strategy": "Count", + "QuantityToKeep": 10, + "ShouldKeepForever": false, + "Unit": "Days" + }` + + runbookRetentionPolicy, err := NewCountBasedRunbookRetentionPolicy(10, RunbookRetentionUnitDays) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + + runbookRetentionPolicyAsJSON, err := json.Marshal(runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicyAsJSON) + + jsonassert.New(t).Assertf(expectedJson, string(runbookRetentionPolicyAsJSON)) +} + +func TestKeepForeverRunbookRetentionPolicyMarshalJSON(t *testing.T) { + expectedJson := `{ + "Strategy": "Forever", + "QuantityToKeep": 0, + "ShouldKeepForever": true, + "Unit": "Items" + }` + + runbookRetentionPolicy := NewKeepForeverRunbookRetentionPolicy() + require.NotNil(t, runbookRetentionPolicy) + runbookRetentionPolicyAsJSON, err := json.Marshal(runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicyAsJSON) + + jsonassert.New(t).Assertf(expectedJson, string(runbookRetentionPolicyAsJSON)) +} + +func TestDefaultRunbookRetentionPolicyMarshalJSON(t *testing.T) { + expectedJson := `{ + "Strategy": "Default", + "QuantityToKeep": 100, + "ShouldKeepForever": false, + "Unit": "Items" + }` + + runbookRetentionPolicy := NewDefaultRunbookRetentionPolicy() + require.NotNil(t, runbookRetentionPolicy) + runbookRetentionPolicyAsJSON, err := json.Marshal(runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicyAsJSON) + + jsonassert.New(t).Assertf(expectedJson, string(runbookRetentionPolicyAsJSON)) +} + +func TestCountBasedRunbookRetentionPolicyWithStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "Strategy": "Count", + "QuantityToKeep": 10, + "Unit": "Days" + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyCount, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(10), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitDays, runbookRetentionPolicy.Unit) +} + +func TestKeepForeverRunbookRetentionPolicyWithStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "Strategy": "Forever", + "QuantityToKeep": 0, + "Unit": "Items", + "ShouldKeepForever": true + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyForever, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(0), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitItems, runbookRetentionPolicy.Unit) +} + +func TestDefaultRunbookRetentionPolicyWithStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "Strategy": "Default", + "QuantityToKeep": 100, + "Unit": "Items", + "ShouldKeepForever": false + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyDefault, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(100), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitItems, runbookRetentionPolicy.Unit) +} + +func TestCountBasedRunbookRetentionPolicyWithoutStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "QuantityToKeep": 10, + "Unit": "Days", + "ShouldKeepForever": false + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyCount, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(10), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitDays, runbookRetentionPolicy.Unit) +} + +func TestKeepForeverRunbookRetentionPolicyWithoutStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "QuantityToKeep": 0, + "Unit": "Items", + "ShouldKeepForever": true + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyForever, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(0), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitItems, runbookRetentionPolicy.Unit) +} + +func TestDefaultRunbookRetentionPolicyWithoutStrategyUnmarshalJSON(t *testing.T) { + inputJSON := `{ + "QuantityToKeep": 100, + "Unit": "Items", + "ShouldKeepForever": false + }` + + var runbookRetentionPolicy RunbookRetentionPolicy + err := json.Unmarshal([]byte(inputJSON), &runbookRetentionPolicy) + require.NoError(t, err) + require.NotNil(t, runbookRetentionPolicy) + require.Equal(t, RunbookRetentionStrategyDefault, runbookRetentionPolicy.Strategy) + require.Equal(t, int32(100), runbookRetentionPolicy.QuantityToKeep) + require.Equal(t, RunbookRetentionUnitItems, runbookRetentionPolicy.Unit) +} From 39af1fa89d956109725d313ecec766845b165d0e Mon Sep 17 00:00:00 2001 From: Hideaki Murakami Date: Tue, 2 Dec 2025 14:59:15 +1300 Subject: [PATCH 2/2] Fix incorrect json tags --- pkg/runbooks/runbook_retention_policy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/runbooks/runbook_retention_policy.go b/pkg/runbooks/runbook_retention_policy.go index f3737234..394fac0e 100644 --- a/pkg/runbooks/runbook_retention_policy.go +++ b/pkg/runbooks/runbook_retention_policy.go @@ -61,7 +61,7 @@ func (r *RunbookRetentionPolicy) MarshalJSON() ([]byte, error) { QuantityToKeep int32 `json:"QuantityToKeep"` ShouldKeepForever bool `json:"ShouldKeepForever"` Unit string `json:"Unit"` - Strategy string `json:"Strategy,omitempty"` + Strategy string `json:"Strategy"` } fields.QuantityToKeep = r.QuantityToKeep @@ -78,7 +78,7 @@ func (r *RunbookRetentionPolicy) UnmarshalJSON(data []byte) error { QuantityToKeep int32 `json:"QuantityToKeep"` ShouldKeepForever bool `json:"ShouldKeepForever"` Unit string `json:"Unit"` - Strategy string `json:"Strategy,omitempty"` + Strategy string `json:"Strategy"` } if err := json.Unmarshal(data, &fields); err != nil {