Skip to content

Commit 386701d

Browse files
authored
Mattc/projectversioning (#48)
* Migrate to the new project versioning strategy * Setting the versioning strategy count parameter * Look up child steps as well * Fixed comments * Updated dependencies * Updated dependencies * Fixed donor package * Reverting to old style of versioning, but leaving the new resources in place. * Fix up variable scoping * Added link to bug * Skip the lookup if there is no deployment process
1 parent 8540669 commit 386701d

File tree

8 files changed

+340
-109
lines changed

8 files changed

+340
-109
lines changed

cmd/internal/converters/deployment_process_converter_base.go

Lines changed: 45 additions & 40 deletions
Large diffs are not rendered by default.

cmd/internal/converters/project_converter.go

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
const octopusdeployProjectsDataType = "octopusdeploy_projects"
2828
const octopusdeployProjectResourceType = "octopusdeploy_project"
29+
const octopusdeployProjectVersioningStrategyResourceType = "octopusdeploy_project_versioning_strategy"
2930

3031
type ProjectConverter struct {
3132
Client client.OctopusClient
@@ -486,6 +487,9 @@ func (c *ProjectConverter) toHcl(project octopus.Project, recursive bool, lookup
486487
return "", err
487488
}
488489

490+
// We'll switch to the new versioning strategy once this bug is resolved:
491+
// https://github.com/OctopusDeploy/terraform-provider-octopusdeploy/issues/55
492+
//versioningStrategy, err := c.convertVersioningStrategyV2(project, projectName, dependencies)
489493
versioningStrategy, err := c.convertVersioningStrategy(project)
490494

491495
if err != nil {
@@ -545,6 +549,11 @@ func (c *ProjectConverter) toHcl(project octopus.Project, recursive bool, lookup
545549
if stateless {
546550
c.writeData(file, "${var."+projectName+"_name}", projectName)
547551
terraformResource.Count = strutil.StrPointer(thisResource.Count)
552+
553+
// This is used by the new versioning strategy resource
554+
//if versioningStrategy != nil {
555+
// versioningStrategy.Count = strutil.StrPointer(thisResource.Count)
556+
//}
548557
}
549558

550559
block := gohcl.EncodeAsBlock(terraformResource, "resource")
@@ -597,6 +606,13 @@ func (c *ProjectConverter) toHcl(project octopus.Project, recursive bool, lookup
597606
}
598607

599608
file.Body().AppendBlock(block)
609+
610+
// This is used by the new versioning strategy resource
611+
//if versioningStrategy != nil {
612+
// versioningStrategyBlock := gohcl.EncodeAsBlock(versioningStrategy, "resource")
613+
// file.Body().AppendBlock(versioningStrategyBlock)
614+
//}
615+
600616
return string(file.Bytes()), nil
601617
}
602618
dependencies.AddResource(thisResource)
@@ -977,6 +993,85 @@ func (c *ProjectConverter) convertUsernamePasswordGitPersistence(project octopus
977993
}
978994
}
979995

996+
// getDeploymentProcessStepId finds the internal ID of the action. This is despite the fact that the API calls the
997+
// parameter "DonorPackageStepId" - it is actually an Action ID, not a step ID.
998+
func (c *ProjectConverter) getDeploymentProcessStepId(project octopus.Project, dependencies *data.ResourceDetailsCollection) *string {
999+
// The first action in a step is combined with the step, so here we lookup the "DeploymentProcesses/Steps" resource type
1000+
stepId := dependencies.GetResourceDependency("DeploymentProcesses/Steps",
1001+
project.Id+"/"+
1002+
strutil.EmptyIfNil(project.DeploymentProcessId)+"/"+
1003+
strutil.EmptyIfNil(project.VersioningStrategy.DonorPackageStepId))
1004+
1005+
// Second and subsequent actions are represented as "DeploymentProcesses/ChildSteps" resources, which we also need to check
1006+
actionId := dependencies.GetResourceDependency("DeploymentProcesses/ChildSteps",
1007+
project.Id+"/"+
1008+
strutil.EmptyIfNil(project.DeploymentProcessId)+"/"+
1009+
strutil.EmptyIfNil(project.VersioningStrategy.DonorPackageStepId))
1010+
1011+
return strutil.NilIfEmpty(strutil.DefaultIfEmpty(stepId, actionId))
1012+
}
1013+
1014+
func (c *ProjectConverter) convertDatabaseVersioningStrategyV2(project octopus.Project, projectName string, dependencies *data.ResourceDetailsCollection) (*terraform.TerraformProjectVersioningStrategy, error) {
1015+
versioningStrategyTerraformResource := terraform.TerraformProjectVersioningStrategy{
1016+
Type: octopusdeployProjectVersioningStrategyResourceType,
1017+
Name: projectName,
1018+
Count: nil,
1019+
ProjectId: "${" + octopusdeployProjectResourceType + "." + projectName + ".id}",
1020+
SpaceId: strutil.InputIfEnabled(c.IncludeSpaceInPopulation, dependencies.GetResourceDependency("Spaces", project.SpaceId)),
1021+
DonorPackageStepId: c.getDeploymentProcessStepId(project, dependencies),
1022+
Template: strutil.NilIfEmpty(project.VersioningStrategy.Template),
1023+
}
1024+
1025+
if project.VersioningStrategy.DonorPackage != nil {
1026+
versioningStrategyTerraformResource.DonorPackage = &terraform.TerraformProjectVersioningStrategyDonorPackage{
1027+
DeploymentAction: strutil.EmptyIfNil(project.VersioningStrategy.DonorPackage.DeploymentAction),
1028+
PackageReference: strutil.EmptyIfNil(project.VersioningStrategy.DonorPackage.PackageReference),
1029+
}
1030+
}
1031+
1032+
return &versioningStrategyTerraformResource, nil
1033+
}
1034+
1035+
func (c *ProjectConverter) convertCaCVersioningStrategyV2(project octopus.Project, projectName string, dependencies *data.ResourceDetailsCollection) (*terraform.TerraformProjectVersioningStrategy, error) {
1036+
deploymentSettings := octopus.ProjectCacDeploymentSettings{}
1037+
if _, err := c.Client.GetResource("Projects/"+project.Id+"/"+project.PersistenceSettings.DefaultBranch+"/DeploymentSettings", &deploymentSettings); err != nil {
1038+
return nil, err
1039+
}
1040+
1041+
versioningStrategyTerraformResource := terraform.TerraformProjectVersioningStrategy{
1042+
Type: octopusdeployProjectVersioningStrategyResourceType,
1043+
Name: projectName,
1044+
Count: nil,
1045+
ProjectId: "${" + octopusdeployProjectResourceType + "." + projectName + ".id}",
1046+
SpaceId: strutil.InputIfEnabled(c.IncludeSpaceInPopulation, dependencies.GetResourceDependency("Spaces", project.SpaceId)),
1047+
DonorPackageStepId: c.getDeploymentProcessStepId(project, dependencies),
1048+
Template: strutil.NilIfEmpty(deploymentSettings.VersioningStrategy.Template),
1049+
}
1050+
1051+
if deploymentSettings.VersioningStrategy.DonorPackage != nil {
1052+
versioningStrategyTerraformResource.DonorPackage = &terraform.TerraformProjectVersioningStrategyDonorPackage{
1053+
DeploymentAction: strutil.EmptyIfNil(deploymentSettings.VersioningStrategy.DonorPackage.DeploymentAction),
1054+
PackageReference: strutil.EmptyIfNil(deploymentSettings.VersioningStrategy.DonorPackage.PackageReference),
1055+
}
1056+
}
1057+
1058+
return &versioningStrategyTerraformResource, nil
1059+
}
1060+
1061+
func (c *ProjectConverter) convertVersioningStrategy(project octopus.Project) (*terraform.TerraformVersioningStrategy, error) {
1062+
if c.IgnoreCacManagedValues && project.HasCacConfigured() {
1063+
return nil, nil
1064+
}
1065+
1066+
// If CaC is enabled, the top level ProjectConnectivityPolicy settings are supplied by the API but ignored..
1067+
// The actual values come from branch specific settings.
1068+
if project.HasCacConfigured() {
1069+
return c.convertCaCVersioningStrategy(project)
1070+
}
1071+
1072+
return c.convertDatabaseVersioningStrategy(project)
1073+
}
1074+
9801075
func (c *ProjectConverter) convertDatabaseVersioningStrategy(project octopus.Project) (*terraform.TerraformVersioningStrategy, error) {
9811076
// Don't define a versioning strategy if it is not set
9821077
if project.VersioningStrategy.Template == "" {
@@ -1034,18 +1129,18 @@ func (c *ProjectConverter) convertCaCVersioningStrategy(project octopus.Project)
10341129
return &versioningStrategy, nil
10351130
}
10361131

1037-
func (c *ProjectConverter) convertVersioningStrategy(project octopus.Project) (*terraform.TerraformVersioningStrategy, error) {
1132+
func (c *ProjectConverter) convertVersioningStrategyV2(project octopus.Project, projectName string, dependencies *data.ResourceDetailsCollection) (*terraform.TerraformProjectVersioningStrategy, error) {
10381133
if c.IgnoreCacManagedValues && project.HasCacConfigured() {
10391134
return nil, nil
10401135
}
10411136

1042-
// If CaC is enabled, the top level ProjectConnectivityPolicy settings are supplied by the API but ignored..
1137+
// If CaC is enabled, the top level ProjectConnectivityPolicy settings are supplied by the API but ignored.
10431138
// The actual values come from branch specific settings.
10441139
if project.HasCacConfigured() {
1045-
return c.convertCaCVersioningStrategy(project)
1140+
return c.convertCaCVersioningStrategyV2(project, projectName, dependencies)
10461141
}
10471142

1048-
return c.convertDatabaseVersioningStrategy(project)
1143+
return c.convertDatabaseVersioningStrategyV2(project, projectName, dependencies)
10491144
}
10501145

10511146
// exportChildDependencies exports those dependencies that are always required regardless of the recursive flag.

cmd/internal/converters/variable_set_converter.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,27 @@ func (c *VariableSetConverter) convertDisplaySettings(prompt octopus.Prompt) *te
15791579
return &display
15801580
}
15811581

1582+
// getDeploymentProcessStepId finds the internal ID of the action. This is despite the fact that the API calls the
1583+
// parameter "DonorPackageStepId" - it is actually an Action ID, not a step ID.
1584+
func (c *VariableSetConverter) getDeploymentProcessStepId(projectId string, deploymentProcessId string, actionIds []string, dependencies *data.ResourceDetailsCollection) []string {
1585+
// If there is no deployment process, there are no action IDs
1586+
if deploymentProcessId == "" {
1587+
return []string{}
1588+
}
1589+
1590+
return lo.Map(actionIds, func(item string, index int) string {
1591+
// The first action in a step is combined with the step, so here we lookup the "DeploymentProcesses/Steps" resource type
1592+
stepDependency := dependencies.GetResource("DeploymentProcesses/Steps",
1593+
projectId+"/"+deploymentProcessId+"/"+item)
1594+
1595+
// Second and subsequent actions are represented as "DeploymentProcesses/ChildSteps" resources, which we also need to check
1596+
actionDependency := dependencies.GetResource("DeploymentProcesses/ChildSteps",
1597+
projectId+"/"+deploymentProcessId+"/"+item)
1598+
1599+
return strutil.DefaultIfEmpty(stepDependency, actionDependency)
1600+
})
1601+
}
1602+
15821603
func (c *VariableSetConverter) convertScope(variable octopus.Variable, variableSet octopus.VariableSet, dependencies *data.ResourceDetailsCollection) (*terraform.TerraformProjectVariableScope, error) {
15831604
filteredEnvironments := c.EnvironmentFilter.FilterEnvironmentScope(variable.Scope.Environment)
15841605

@@ -1587,28 +1608,17 @@ func (c *VariableSetConverter) convertScope(variable octopus.Variable, variableS
15871608
zap.L().Warn("WARNING: Variable " + variable.Name + " removed all environment scopes.")
15881609
}
15891610

1590-
// Get a list of action IDs that includes the deployment process ID. This is because action IDs are
1591-
// not guaranteed to be unique and are saved in the dependencies with the deployment process ID as a prefix.
1592-
// If the variable set belongs to a library variable set, it can't be scoped to action, and so the profix
1593-
// won't be used.
1594-
actionPrefix, err := c.getDeploymentProcessFromVariable(variableSet)
1611+
deploymentProcessId, err := c.getDeploymentProcessFromVariable(variableSet)
15951612

15961613
if err != nil {
15971614
return nil, err
15981615
}
15991616

1600-
fixedActionIds := lo.Map(variable.Scope.Action, func(actionId string, index int) string {
1601-
return actionPrefix + "-" + actionId
1602-
})
1603-
1604-
// We should always find a prefix if there were any variable scopes. There may be edge cases
1605-
// where the action was deleted after the variable was created, for example a CaC project edited
1606-
// the deployment process but not the variable scopes.
1607-
if len(actionPrefix) == 0 && len(variable.Scope.Action) != 0 {
1608-
zap.L().Warn("WARNING: Variable " + variable.Name + " has action scopes but we did not find the deployment process. This means variables arr scoped to actions that do not exist.")
1609-
}
1610-
1611-
actions := dependencies.GetResources("Actions", fixedActionIds...)
1617+
// BUG: A octopusdeploy_process_child_step includes both the step and the first action. Hwoever, the ID is the step ID.
1618+
// There is no way to get the ID of the action. This means that any variables scoped to the first step in a process
1619+
// will be recreated with the step ID, and not the action ID, leading to a "Missing Resource" error in the UI.
1620+
// See https://github.com/OctopusDeploy/terraform-provider-octopusdeploy/issues/60
1621+
actions := c.getDeploymentProcessStepId(strutil.EmptyIfNil(variableSet.OwnerId), deploymentProcessId, variable.Scope.Action, dependencies)
16121622
channels := dependencies.GetResources("Channels", variable.Scope.Channel...)
16131623
environments := dependencies.GetResources("Environments", filteredEnvironments...)
16141624
machines := dependencies.GetResources("Machines", variable.Scope.Machine...)

cmd/internal/data/export_map.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package data
22

33
import (
4+
"sync"
5+
46
"github.com/OctopusSolutionsEngineering/OctopusTerraformExport/cmd/internal/strutil"
57
"github.com/samber/lo"
68
"go.uber.org/zap"
7-
"sync"
89
)
910

1011
type ToHcl func() (string, error)
@@ -29,7 +30,13 @@ type ResourceParameter struct {
2930
type ResourceDetails struct {
3031
// Id is the octopus ID of the exported resource
3132
Id string
32-
// ParentId is an optional field that allows a resource to define its parenr.
33+
// AlternateId is the alternate octopus ID of the exported resource.
34+
// This can occur when a single Terraform resource represents multiple Octopus resources.
35+
// An example is an octopusdeploy_process_step resource, which represents the combination of a step
36+
// and the first action in the step. Sometimes we need to reference the step by its ID, and sometimes
37+
// we need to reference the action by its ID.
38+
AlternateId string
39+
// ParentId is an optional field that allows a resource to define its parent.
3340
// This is useful when establishing dependencies between Terraform resources where it is not easy to identify the
3441
// individual Terraform resources that belong to a parent. For example, a channel must depend on the steps in a project
3542
// because a channel references step packages by name, and thus do not establish a direct relationship that can be
@@ -161,7 +168,7 @@ func (c *ResourceDetailsCollection) GetResource(resourceType string, id string)
161168
defer c.mu.Unlock()
162169

163170
for _, r := range c.Resources {
164-
if r.Id == id && r.ResourceType == resourceType {
171+
if (r.Id == id || r.AlternateId == id) && r.ResourceType == resourceType {
165172
return r.Lookup
166173
}
167174
}
@@ -247,7 +254,7 @@ func (c *ResourceDetailsCollection) GetResourceDependency(resourceType string, i
247254
defer c.mu.Unlock()
248255

249256
for _, r := range c.Resources {
250-
if r.Id == id && r.ResourceType == resourceType {
257+
if (r.Id == id || r.AlternateId == id) && r.ResourceType == resourceType {
251258
// return the dependency field if it was defined, otherwise fall back to the lookup field
252259
return strutil.DefaultIfEmpty(r.Dependency, r.Lookup)
253260
}
@@ -258,6 +265,29 @@ func (c *ResourceDetailsCollection) GetResourceDependency(resourceType string, i
258265
return ""
259266
}
260267

268+
// GetResourceDependencyPointer returns the terraform references for a given resource type and id.
269+
// The returned string is used only for the depends_on field, as it may reference to a collection of resources
270+
// rather than a single ID.
271+
func (c *ResourceDetailsCollection) GetResourceDependencyPointer(resourceType string, id *string) *string {
272+
if id == nil {
273+
return nil
274+
}
275+
276+
c.mu.Lock()
277+
defer c.mu.Unlock()
278+
279+
for _, r := range c.Resources {
280+
if r.Id == *id && r.ResourceType == resourceType {
281+
// return the dependency field if it was defined, otherwise fall back to the lookup field
282+
return strutil.NilIfEmpty(strutil.DefaultIfEmpty(r.Dependency, r.Lookup))
283+
}
284+
}
285+
286+
zap.L().Error("Failed to resolve dependency " + *id + " of type " + resourceType)
287+
288+
return nil
289+
}
290+
261291
// GetResourceDependencyFromParent returns the terraform references for a given resource type based on the parent ID.
262292
func (c *ResourceDetailsCollection) GetResourceDependencyFromParent(parentId string, resourceType string) []string {
263293
c.mu.Lock()

cmd/internal/model/terraform/terraform_project.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@ type TerraformProject struct {
2323
GitLibraryPersistenceSettings *TerraformGitLibraryPersistenceSettings `hcl:"git_library_persistence_settings,block"`
2424
GitAnonymousPersistenceSettings *TerraformGitAnonymousPersistenceSettings `hcl:"git_anonymous_persistence_settings,block"`
2525
GitUsernamePasswordPersistenceSettings *TerraformGitUsernamePasswordPersistenceSettings `hcl:"git_username_password_persistence_settings,block"`
26-
VersioningStrategy *TerraformVersioningStrategy `hcl:"versioning_strategy,block"`
2726
Lifecycle *TerraformLifecycleMetaArgument `hcl:"lifecycle,block"`
27+
VersioningStrategy *TerraformVersioningStrategy `hcl:"versioning_strategy,block"`
28+
}
29+
30+
type TerraformVersioningStrategy struct {
31+
Template string `hcl:"template"`
32+
DonorPackageStepId *string `hcl:"donor_package_step_id"`
33+
DonorPackage *TerraformDonorPackage `hcl:"donor_package,block"`
34+
}
35+
36+
type TerraformDonorPackage struct {
37+
DeploymentAction *string `hcl:"deployment_action"`
38+
PackageReference *string `hcl:"package_reference"`
2839
}
2940

3041
func (t TerraformProject) HasCacConfigured() bool {
@@ -73,17 +84,6 @@ type TerraformGitUsernamePasswordPersistenceSettings struct {
7384
ProtectedBranches string `hcl:"protected_branches"`
7485
}
7586

76-
type TerraformVersioningStrategy struct {
77-
Template string `hcl:"template"`
78-
DonorPackageStepId *string `hcl:"donor_package_step_id"`
79-
DonorPackage *TerraformDonorPackage `hcl:"donor_package,block"`
80-
}
81-
82-
type TerraformDonorPackage struct {
83-
DeploymentAction *string `hcl:"deployment_action"`
84-
PackageReference *string `hcl:"package_reference"`
85-
}
86-
8787
type TerraformLifecycleMetaArgument struct {
8888
CreateBeforeDestroy *bool `hcl:"create_before_destroy"`
8989
IgnoreChanges *[]string `hcl:"ignore_changes"`
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package terraform
2+
3+
type TerraformProjectVersioningStrategy struct {
4+
Type string `hcl:"type,label"`
5+
Name string `hcl:"name,label"`
6+
Count *string `hcl:"count"`
7+
ProjectId string `hcl:"project_id"`
8+
SpaceId *string `hcl:"space_id"`
9+
DonorPackage *TerraformProjectVersioningStrategyDonorPackage `hcl:"donor_package,block"`
10+
DonorPackageStepId *string `hcl:"donor_package_step_id"`
11+
Template *string `hcl:"template"`
12+
}
13+
14+
type TerraformProjectVersioningStrategyDonorPackage struct {
15+
DeploymentAction string `hcl:"deployment_action"`
16+
PackageReference string `hcl:"package_reference"`
17+
}

0 commit comments

Comments
 (0)