Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
588ab66
Edit attribute descriptions
Rose-Northey Sep 10, 2025
1035f92
edit to docs
Rose-Northey Sep 10, 2025
222736d
refactor validation
Rose-Northey Sep 10, 2025
711c024
Justify unit error
Rose-Northey Sep 10, 2025
f53bbe4
move retention tests to a seperate lifecycle retention testing page
Rose-Northey Sep 10, 2025
6746323
add testing for current state
Rose-Northey Sep 10, 2025
08c7f7e
Merge branch 'main' into fnm/add-testing-to-lifecycle-retention-policies
Rose-Northey Sep 10, 2025
b619288
add two more test cases
Rose-Northey Sep 10, 2025
57b64db
Merge branch 'fnm/add-testing-to-lifecycle-retention-policies' into f…
Rose-Northey Sep 10, 2025
2244fba
merge main into branch
Rose-Northey Sep 10, 2025
c73d9dc
add test for empty block producing an error
Rose-Northey Sep 10, 2025
c313a13
Merge branch 'fnm/add-testing-to-lifecycle-retention-policies' into f…
Rose-Northey Sep 10, 2025
49b373c
error messages occuring at api made vaguer
Rose-Northey Sep 10, 2025
bad1e9a
Merge branch 'fnm/add-testing-to-lifecycle-retention-policies' into f…
Rose-Northey Sep 10, 2025
7ce2d26
tweak to description
Rose-Northey Sep 10, 2025
a9b2d78
make error messages more specific
Rose-Northey Sep 11, 2025
f7c23bc
add "empty error" message
Rose-Northey Sep 11, 2025
84df380
Merge branch 'fnm/add-testing-to-lifecycle-retention-policies' into f…
Rose-Northey Sep 11, 2025
e0e56be
update error messages according to refactor
Rose-Northey Sep 12, 2025
154e62d
Merge branch 'main' into fnm/refactor-and-fix-lifecycle-retention-pol…
Rose-Northey Sep 12, 2025
76b7697
Merge branch 'main' into fnm/refactor-and-fix-lifecycle-retention-pol…
Rose-Northey Sep 12, 2025
3781576
merge into single boolean
Rose-Northey Sep 12, 2025
6fe0ed3
validate units within schema and test
Rose-Northey Sep 12, 2025
92492c5
delete redundant code now that units are validated in schema
Rose-Northey Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions docs/resources/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,19 @@ Optional:

Optional:

- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days.
- `quantity_to_keep` (Number) The number of days/releases to keep. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items.


<a id="nestedblock--phase--tentacle_retention_policy"></a>
### Nested Schema for `phase.tentacle_retention_policy`

Optional:

- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days.
- `quantity_to_keep` (Number) The number of days/releases to keep. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items.



Expand All @@ -116,19 +116,19 @@ Optional:

Optional:

- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days.
- `quantity_to_keep` (Number) The number of days/releases to keep. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items.


<a id="nestedblock--tentacle_retention_policy"></a>
### Nested Schema for `tentacle_retention_policy`

Optional:

- `quantity_to_keep` (Number) The number of days/releases to keep. The default value is 30. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted. The default value is false.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items. The default value is Days.
- `quantity_to_keep` (Number) The number of days/releases to keep. If 0 then all are kept.
- `should_keep_forever` (Boolean) Indicates if items should never be deleted.
- `unit` (String) The unit of quantity to keep. Valid units are Days or Items.

## Import

Expand Down
19 changes: 13 additions & 6 deletions octopusdeploy_framework/resource_lifecycle_retention_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,33 +117,40 @@ func TestAccRetentionAttributeValidation(t *testing.T) {
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "1", "Items", "true"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`should_keep_forever must be false when quantity_to_keep is not 0`),
ExpectError: regexp.MustCompile(`should_keep_forever must be false when quantity_to_keep is greater than 0`),
},
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "1", "", "true"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`should_keep_forever must be false when quantity_to_keep is not 0`),
ExpectError: regexp.MustCompile(`should_keep_forever must be false when quantity_to_keep is greater than 0`),
},
// when quantity_to_keep is 0, should_keep_forever shouldn't be false
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "0", "", "false"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`should_keep_forever must be true when quantity_to_keep is 0`),
ExpectError: regexp.MustCompile(`should_keep_forever must be true when quantity_to_keep is zero or missing`),
},
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "", "", "false"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`The non-refresh plan was not empty`),
ExpectError: regexp.MustCompile(`should_keep_forever must be true when quantity_to_keep is zero or missing`),
},
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "", "Items", "false"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`The non-refresh plan was not empty`),
ExpectError: regexp.MustCompile(`should_keep_forever must be true when quantity_to_keep is zero or missing`),
},
//should thow error when something other than days or items is submitted for units
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "4", "Months", "false"),
PlanOnly: true,
ExpectError: regexp.MustCompile(`Invalid Attribute Value Match`),
},
//should throw error when empty block is given
{
Config: lifecycleGivenRetentionAttributes(lifecycleName, "", "", ""),
PlanOnly: true,
ExpectError: regexp.MustCompile(`The non-refresh plan was not empty`),
ExpectError: regexp.MustCompile(`please either add retention policy attributes or remove the entire block`),
},
},
})
Expand Down
37 changes: 0 additions & 37 deletions octopusdeploy_framework/resource_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,25 +254,6 @@ func TestAccLifecycleWithUpdate(t *testing.T) {
),
Config: testAccLifecycle(localName, name),
},
// update lifecycle add retention policy
{
Check: resource.ComposeTestCheckFunc(
testAccCheckLifecycleExists(resourceName),
resource.TestCheckResourceAttrSet(resourceName, "id"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "description", description),
resource.TestCheckResourceAttr(resourceName, "release_retention_policy.#", "1"),
resource.TestCheckResourceAttr(resourceName, "release_retention_policy.0.quantity_to_keep", "60"),
resource.TestCheckResourceAttr(resourceName, "release_retention_policy.0.should_keep_forever", "false"),
resource.TestCheckResourceAttr(resourceName, "release_retention_policy.0.unit", "Days"),
resource.TestCheckResourceAttrSet(resourceName, "space_id"),
resource.TestCheckResourceAttr(resourceName, "tentacle_retention_policy.#", "1"),
resource.TestCheckResourceAttr(resourceName, "tentacle_retention_policy.0.quantity_to_keep", "0"),
resource.TestCheckResourceAttr(resourceName, "tentacle_retention_policy.0.should_keep_forever", "true"),
resource.TestCheckResourceAttr(resourceName, "tentacle_retention_policy.0.unit", "Items"),
),
Config: testAccLifecycleWithRetentionPolicy(localName, name, description),
},
},
})
}
Expand Down Expand Up @@ -324,24 +305,6 @@ func testAccLifecycleWithDescription(localName string, name string, description
}`, localName, name, description)
}

func testAccLifecycleWithRetentionPolicy(localName string, name string, description string) string {
return fmt.Sprintf(`resource "octopusdeploy_lifecycle" "%s" {
name = "%s"
description = "%s"
release_retention_policy {
unit = "Days"
quantity_to_keep = 60
should_keep_forever = false
}

tentacle_retention_policy {
unit = "Items"
quantity_to_keep = 0
should_keep_forever = true
}
}`, localName, name, description)
}

func testAccLifecycleComplex(localName string, name string) string {
environment1LocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
environment1Name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
Expand Down
63 changes: 42 additions & 21 deletions octopusdeploy_framework/schemas/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package schemas

import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"strings"

"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
Expand All @@ -13,7 +14,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
Expand Down Expand Up @@ -111,19 +111,16 @@ func getResourceRetentionPolicyBlockSchema() resourceSchema.ListNestedBlock {
Attributes: map[string]resourceSchema.Attribute{
"quantity_to_keep": util.ResourceInt64().
Optional().Computed().
Default(int64default.StaticInt64(30)).
Validators(int64validator.AtLeast(0)).
Description("The number of days/releases to keep. The default value is 30. If 0 then all are kept.").
Description("The number of days/releases to keep. If 0 then all are kept.").
Build(),
"should_keep_forever": util.ResourceBool().
Optional().Computed().
Default(booldefault.StaticBool(false)).
Description("Indicates if items should never be deleted. The default value is false.").
Description("Indicates if items should never be deleted.").
Build(),
"unit": util.ResourceString().
Optional().Computed().
Default(stringdefault.StaticString("Days")).
Description("The unit of quantity to keep. Valid units are Days or Items. The default value is Days.").
Optional().Computed().Validators(stringvalidator.OneOfCaseInsensitive("Days", "Items")).
Description("The unit of quantity to keep. Valid units are Days or Items.").
Build(),
},
Validators: []validator.Object{
Expand Down Expand Up @@ -206,32 +203,56 @@ func (v retentionPolicyValidator) ValidateObject(ctx context.Context, req valida
return
}

if !retentionPolicy.QuantityToKeep.IsNull() && !retentionPolicy.QuantityToKeep.IsUnknown() && !retentionPolicy.ShouldKeepForever.IsNull() && !retentionPolicy.ShouldKeepForever.IsUnknown() {
quantityToKeep := retentionPolicy.QuantityToKeep.ValueInt64()
shouldKeepForever := retentionPolicy.ShouldKeepForever.ValueBool()
unitPresent := !retentionPolicy.Unit.IsNull() && !retentionPolicy.Unit.IsUnknown()
quantityToKeepPresent := !retentionPolicy.QuantityToKeep.IsNull() && !retentionPolicy.QuantityToKeep.IsUnknown()
shouldKeepForeverPresent := !retentionPolicy.ShouldKeepForever.IsNull() && !retentionPolicy.ShouldKeepForever.IsUnknown()
shouldKeepForever := shouldKeepForeverPresent && retentionPolicy.ShouldKeepForever.ValueBool()
quantityToKeepIsMoreThanZero := quantityToKeepPresent && retentionPolicy.QuantityToKeep.ValueInt64() > 0

if quantityToKeep == 0 && !shouldKeepForever {
if !unitPresent && !quantityToKeepPresent && !shouldKeepForeverPresent {
resp.Diagnostics.AddAttributeError(
req.Path.AtName("strategy"),
"Invalid retention policy configuration",
"please either add retention policy attributes or remove the entire block",
)
}

// count strategy validations
if quantityToKeepIsMoreThanZero {
if shouldKeepForever {
resp.Diagnostics.AddAttributeError(
req.Path.AtName("should_keep_forever"),
"Invalid retention policy configuration",
"should_keep_forever must be true when quantity_to_keep is 0",
"should_keep_forever must be false when quantity_to_keep is greater than 0",
)
} else if quantityToKeep != 0 && shouldKeepForever {
}
if !unitPresent {
resp.Diagnostics.AddAttributeError(
req.Path.AtName("should_keep_forever"),
req.Path.AtName("unit"),
"Invalid retention policy configuration",
"should_keep_forever must be false when quantity_to_keep is not 0",
"unit is required when quantity_to_keep is greater than 0",
)
}
}

if !retentionPolicy.Unit.IsNull() && !retentionPolicy.Unit.IsUnknown() {
unit := retentionPolicy.Unit.ValueString()
if !strings.EqualFold(unit, "Days") && !strings.EqualFold(unit, "Items") {
// keep forever strategy validation
if !quantityToKeepIsMoreThanZero && !shouldKeepForever {
resp.Diagnostics.AddAttributeError(
req.Path.AtName("should_keep_forever"),
"Invalid retention policy configuration",
"should_keep_forever must be true when quantity_to_keep is zero or missing",
)
}

if unitPresent && !quantityToKeepIsMoreThanZero {
if strings.EqualFold(retentionPolicy.Unit.ValueString(), "Items") {
// do not throw an error for backwards compatability.
} else {
resp.Diagnostics.AddAttributeError(
// replaces a confusing state change to "unit = Items" error at the api
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is to deal with the case sensitive API error response for "Items", but the error message is not related to this at all, which is confusing. Could you explain what you mean here by this comment?

Copy link
Contributor Author

@Rose-Northey Rose-Northey Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the user puts days in when the strategy isn't set to count, the server will return "unexpected result, unit was Days but is now Items" because the server returns items no matter what when a forever or default strategy is set.

It won't error when people use "items" even though items isn't needed so I thought it would be good to not break instances that are doing this (even though its an unnecessary field)

I was told to use (stringvalidator.OneOfCaseInsensitive("Days", "Items")) in the schema instead of letting the validation logic be in the external validator so that's why lines 233 and 234 are deleted below

req.Path.AtName("unit"),
"Invalid retention policy unit",
"Unit must be either 'Days' or 'Items' (case insensitive)",
"Invalid retention policy configuration",
"unit is only used when quantity_to_keep is greater than 0",
)
}
}
Expand Down
Loading