-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: deployment freezes #822
base: main
Are you sure you want to change the base?
Changes from 10 commits
cf62bb2
fb919c6
1ce7c08
63c094b
9b1ee2f
879becd
84b7fe4
a67a0b6
b2eb8d6
c761241
1aa7b68
e9da4d6
9b70c40
7660848
c22a0c0
70f2a6b
f4ac55b
96dc497
e31e4f4
efd1d83
9e504eb
b834bc4
91da299
268cf29
a82d9d7
dfc11c4
872fc77
7deb89d
0b61d02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
package octopusdeploy_framework | ||
|
||
import ( | ||
"context" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes" | ||
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" | ||
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" | ||
"github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" | ||
"github.com/hashicorp/terraform-plugin-framework/diag" | ||
"github.com/hashicorp/terraform-plugin-framework/resource" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"time" | ||
) | ||
|
||
type deploymentFreezeModel struct { | ||
Name types.String `tfsdk:"name"` | ||
Start types.String `tfsdk:"start"` | ||
End types.String `tfsdk:"end"` | ||
ProjectEnvironmentScope types.Map `tfsdk:"project_environment_scope"` | ||
|
||
schemas.ResourceModel | ||
} | ||
|
||
type deploymentFreezeResource struct { | ||
*Config | ||
} | ||
|
||
var _ resource.Resource = &deploymentFreezeResource{} | ||
|
||
func NewDeploymentFreezeResource() resource.Resource { | ||
return &deploymentFreezeResource{} | ||
} | ||
|
||
func (f *deploymentFreezeResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { | ||
resp.TypeName = util.GetTypeName(schemas.DeploymentFreezeResourceName) | ||
} | ||
|
||
func (f *deploymentFreezeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
resp.Schema = schemas.DeploymentFreezeSchema{}.GetResourceSchema() | ||
} | ||
|
||
func (f *deploymentFreezeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { | ||
f.Config = ResourceConfiguration(req, resp) | ||
} | ||
|
||
func (f *deploymentFreezeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
var state *deploymentFreezeModel | ||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
deploymentFreeze, err := deploymentfreezes.GetById(f.Config.Client, state.GetID()) | ||
if err != nil { | ||
if err := errors.ProcessApiErrorV2(ctx, resp, state, err, "deployment freeze"); err != nil { | ||
resp.Diagnostics.AddError("unable to load deployment freeze", err.Error()) | ||
} | ||
return | ||
} | ||
|
||
diags := mapToState(ctx, state, deploymentFreeze, true) | ||
if diags.HasError() { | ||
resp.Diagnostics = diags | ||
return | ||
} | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) | ||
} | ||
|
||
func (f *deploymentFreezeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
var plan *deploymentFreezeModel | ||
diags := req.Plan.Get(ctx, &plan) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
var deploymentFreeze *deploymentfreezes.DeploymentFreeze | ||
deploymentFreeze, err := mapFromState(plan) | ||
if err != nil { | ||
resp.Diagnostics.AddError("error while creating deployment freeze", err.Error()) | ||
return | ||
} | ||
|
||
createdFreeze, err := deploymentfreezes.Add(f.Config.Client, deploymentFreeze) | ||
if err != nil { | ||
resp.Diagnostics.AddError("error while creating deployment freeze", err.Error()) | ||
return | ||
} | ||
|
||
diags = mapToState(ctx, plan, createdFreeze, false) | ||
if diags.HasError() { | ||
return | ||
} | ||
|
||
diags = resp.State.Set(ctx, plan) | ||
resp.Diagnostics.Append(diags...) | ||
} | ||
|
||
func (f *deploymentFreezeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
var plan *deploymentFreezeModel | ||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
existingFreeze, err := deploymentfreezes.GetById(f.Config.Client, plan.ID.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError("unable to load deployment freeze", err.Error()) | ||
return | ||
} | ||
|
||
updatedFreeze, err := mapFromState(plan) | ||
if err != nil { | ||
resp.Diagnostics.AddError("error while mapping deployment freeze", err.Error()) | ||
} | ||
|
||
updatedFreeze.SetID(existingFreeze.GetID()) | ||
updatedFreeze.Links = existingFreeze.Links | ||
|
||
updatedFreeze, err = deploymentfreezes.Update(f.Config.Client, updatedFreeze) | ||
if err != nil { | ||
resp.Diagnostics.AddError("error while updating deployment freeze", err.Error()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing return? |
||
} | ||
|
||
diags := mapToState(ctx, plan, updatedFreeze, false) | ||
if diags.HasError() { | ||
return | ||
} | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) | ||
} | ||
|
||
func (f *deploymentFreezeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
var state *deploymentFreezeModel | ||
diags := req.State.Get(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
freeze, err := deploymentfreezes.GetById(f.Config.Client, state.GetID()) | ||
if err != nil { | ||
resp.Diagnostics.AddError("unable to load deployment freeze", err.Error()) | ||
return | ||
} | ||
|
||
err = deploymentfreezes.Delete(f.Config.Client, freeze) | ||
if err != nil { | ||
resp.Diagnostics.AddError("unable to delete deployment freeze", err.Error()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return? |
||
} | ||
|
||
resp.State.RemoveResource(ctx) | ||
} | ||
|
||
func mapToState(ctx context.Context, state *deploymentFreezeModel, deploymentFreeze *deploymentfreezes.DeploymentFreeze, useSourceForDates bool) diag.Diagnostics { | ||
state.ID = types.StringValue(deploymentFreeze.ID) | ||
state.Name = types.StringValue(deploymentFreeze.Name) | ||
if useSourceForDates { | ||
state.Start = types.StringValue(deploymentFreeze.Start.Format(time.RFC3339)) | ||
state.End = types.StringValue(deploymentFreeze.End.Format(time.RFC3339)) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the time contains a timezone the value for the times are returned with a server local timezone, for create and update operations we just store the value provided in the plan, for a refresh (read) operation it will store the value from the api as it may have been changed. |
||
|
||
if len(deploymentFreeze.ProjectEnvironmentScope) > 0 { | ||
value, diags := util.ConvertMapStringArrayToMapAttrValue(ctx, deploymentFreeze.ProjectEnvironmentScope) | ||
if diags.HasError() { | ||
return diags | ||
} | ||
state.ProjectEnvironmentScope, diags = types.MapValueFrom(ctx, types.SetType{ElemType: types.StringType}, value) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func mapFromState(state *deploymentFreezeModel) (*deploymentfreezes.DeploymentFreeze, error) { | ||
start, err := time.Parse(time.RFC3339, state.Start.ValueString()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
end, err := time.Parse(time.RFC3339, state.End.ValueString()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
start = start.UTC() | ||
end = end.UTC() | ||
|
||
freeze := deploymentfreezes.DeploymentFreeze{ | ||
Name: state.Name.ValueString(), | ||
Start: &start, | ||
End: &end, | ||
} | ||
|
||
freeze.ID = state.ID.String() | ||
if !state.ProjectEnvironmentScope.IsNull() { | ||
scopeMap := make(map[string][]string) | ||
for k, v := range state.ProjectEnvironmentScope.Elements() { | ||
var scopes []string | ||
for _, s := range v.(types.Set).Elements() { | ||
scopes = append(scopes, s.(types.String).ValueString()) | ||
} | ||
|
||
scopeMap[k] = scopes | ||
} | ||
freeze.ProjectEnvironmentScope = scopeMap | ||
} | ||
|
||
return &freeze, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package octopusdeploy_framework | ||
|
||
import ( | ||
"fmt" | ||
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deploymentfreezes" | ||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest" | ||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
"github.com/hashicorp/terraform-plugin-testing/terraform" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestNewDeploymentFreezeResource(t *testing.T) { | ||
localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
resourceName := "octopusdeploy_deployment_freeze." + localName | ||
name := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
start := fmt.Sprintf("%d-11-21T06:30:00+10:00", time.Now().Year()+1) | ||
end := fmt.Sprintf("%d-11-21T08:30:00+10:00", time.Now().Year()+1) | ||
projectName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
environmentName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
spaceName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
projectGroupName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
lifecycleName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) | ||
|
||
resource.Test(t, resource.TestCase{ | ||
CheckDestroy: testDeploymentFreezeCheckDestroy, | ||
PreCheck: func() { TestAccPreCheck(t) }, | ||
ProtoV6ProviderFactories: ProtoV6ProviderFactories(), | ||
Steps: []resource.TestStep{ | ||
{ | ||
Check: resource.ComposeTestCheckFunc( | ||
testDeploymentFreezeExists(resourceName), | ||
resource.TestCheckResourceAttr(resourceName, "name", name), | ||
resource.TestCheckResourceAttr(resourceName, "start", start), | ||
resource.TestCheckResourceAttr(resourceName, "end", end)), | ||
Config: testDeploymentFreezeBasic(localName, name, start, end, spaceName, environmentName, projectName, projectGroupName, lifecycleName), | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add one more step so we can also test update. |
||
}, | ||
}) | ||
} | ||
|
||
func testDeploymentFreezeBasic(localName string, freezeName string, start string, end string, spaceName string, environmentName string, projectName string, projectGroupName string, lifecycleName string) string { | ||
spaceLocalName := fmt.Sprintf("space_%s", localName) | ||
environmentLocalName := fmt.Sprintf("environment_%s", localName) | ||
projectLocalName := fmt.Sprintf("project_%s", localName) | ||
lifecycleLocalName := fmt.Sprintf("lifecycle_%s", localName) | ||
projectGroupLocalName := fmt.Sprintf("project_group_%s", localName) | ||
|
||
projectScopes := fmt.Sprintf(`{ | ||
"${resource.octopusdeploy_project.%s.id}" = [ resource.octopusdeploy_environment.%s.id | ||
}`, projectLocalName, environmentLocalName) | ||
|
||
return fmt.Sprintf(` | ||
%s | ||
|
||
%s | ||
|
||
%s | ||
|
||
%s | ||
|
||
%s | ||
|
||
resource "octopusdeploy_deployment_freeze" "%s" { | ||
name = "%s" | ||
start = "%s" | ||
end = "%s" | ||
project_environment_scope = { | ||
%s | ||
}`, | ||
createSpace(spaceLocalName, spaceName), | ||
createEnvironment(spaceLocalName, environmentLocalName, environmentName), | ||
createLifecycle(spaceLocalName, lifecycleLocalName, lifecycleName), | ||
createProjectGroup(spaceLocalName, projectGroupLocalName, projectGroupName), | ||
createProject(spaceLocalName, projectLocalName, projectName, lifecycleLocalName, projectGroupLocalName), | ||
localName, freezeName, start, end, projectScopes) | ||
} | ||
|
||
func testDeploymentFreezeExists(prefix string) resource.TestCheckFunc { | ||
return func(s *terraform.State) error { | ||
freezeId := s.RootModule().Resources[prefix].Primary.ID | ||
if _, err := deploymentfreezes.GetById(octoClient, freezeId); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
func testDeploymentFreezeCheckDestroy(s *terraform.State) error { | ||
for _, rs := range s.RootModule().Resources { | ||
if rs.Type != "octopusdeploy_deployment_freeze" { | ||
continue | ||
} | ||
|
||
feed, err := deploymentfreezes.GetById(octoClient, rs.Primary.ID) | ||
if err == nil && feed != nil { | ||
return fmt.Errorf("Deployment Freeze (%s) still exists", rs.Primary.ID) | ||
} | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -260,7 +260,7 @@ func flattenTemplates(templates []actiontemplates.ActionTemplateParameter) types | |
"default_value": util.StringOrNull(template.DefaultValue.Value), | ||
"display_settings": types.MapValueMust( | ||
types.StringType, | ||
convertMapStringToMapAttrValue(template.DisplaySettings), | ||
util.ConvertMapStringToMapAttrValue(template.DisplaySettings), | ||
), | ||
}) | ||
|
||
|
@@ -310,14 +310,6 @@ func flattenReleaseCreationStrategy(strategy *projects.ReleaseCreationStrategy) | |
return types.ListValueMust(types.ObjectType{AttrTypes: getReleaseCreationStrategyAttrTypes()}, []attr.Value{obj}) | ||
} | ||
|
||
func convertMapStringToMapAttrValue(m map[string]string) map[string]attr.Value { | ||
result := make(map[string]attr.Value, len(m)) | ||
for k, v := range m { | ||
result[k] = types.StringValue(v) | ||
} | ||
return result | ||
} | ||
Comment on lines
-313
to
-319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was moved to |
||
|
||
func flattenDeploymentActionPackage(pkg *packages.DeploymentActionPackage) types.List { | ||
if pkg == nil { | ||
return types.ListNull(types.ObjectType{AttrTypes: getDonorPackageAttrTypes()}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing return?