Skip to content

Commit

Permalink
update reconcileAliases to use opslevel-go implementation (#383)
Browse files Browse the repository at this point in the history
* update reconcileAliases to use opslevel-go implementation

* minor fix

* update reconcile aliases logic

* add warning about infinitely new aliases on creation

* add changie log

* update note on aliases in examples
  • Loading branch information
davidbloss authored Jun 26, 2024
1 parent 0005359 commit b72b08d
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 203 deletions.
3 changes: 3 additions & 0 deletions .changes/unreleased/Bugfix-20240626-140311.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Bugfix
body: fix management of aliases on Teams and Services. NOTE - aliases in Terraform config now require aliases generated from API. Missing aliases will be displayed by warning messages
time: 2024-06-26T14:03:11.435544-05:00
3 changes: 3 additions & 0 deletions .changes/unreleased/Feature-20240624-160020.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Feature
body: add TagSetValueToTagSlice to convert tags as strings to slice of opslevel.Tag
time: 2024-06-24T16:00:20.808257-05:00
5 changes: 3 additions & 2 deletions examples/resources/opslevel_infrastructure/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ resource "opslevel_infrastructure" "example_1" {

// Detailed example
resource "opslevel_infrastructure" "example_2" {
schema = "Database"
owner = data.opslevel_team.foo.id
aliases = ["foo", "bar", "baz"]
schema = "Database"
owner = data.opslevel_team.foo.id
provider_data = {
account = "dev"
name = "google cloud"
Expand Down
4 changes: 2 additions & 2 deletions examples/resources/opslevel_service/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ data "opslevel_tier" "tier3" {
resource "opslevel_team" "foo" {
name = "foo"
responsibilities = "Responsible for foo frontend and backend"
aliases = ["bar", "baz"]
aliases = ["foo", "bar", "baz"] # NOTE: if set, slugified value of "name" must be included

member {
email = "john.doe@example.com"
Expand All @@ -37,7 +37,7 @@ resource "opslevel_service" "foo" {
api_document_path = "/swagger.json"
preferred_api_document_source = "PULL" //or "PUSH"

aliases = ["bar", "baz"]
aliases = ["foo", "bar", "baz"] # NOTE: if set, value of "name" must be included
tags = ["foo:bar"]
}

Expand Down
2 changes: 1 addition & 1 deletion examples/resources/opslevel_team/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ data "opslevel_team" "parent" {
resource "opslevel_team" "example" {
name = "foo"
responsibilities = "Responsible for foo frontend and backend"
aliases = ["bar", "baz"]
aliases = ["foo", "bar", "baz"] # NOTE: if set, slugified value of "name" must be included
parent = data.opslevel_team.parent.id

member {
Expand Down
51 changes: 51 additions & 0 deletions opslevel/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package opslevel
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
Expand Down Expand Up @@ -111,6 +113,55 @@ func FilterAttrs(validFieldNames []string) map[string]schema.Attribute {
return filterAttrs
}

// return strings not in both slices
func diffBetweenStringSlices(sliceOne, sliceTwo []string) []string {
var diffValues []string

// collect values that are in sliceOne but not in sliceTwo
for _, value := range sliceOne {
if !slices.Contains(sliceTwo, value) {
diffValues = append(diffValues, value)
}
}

// collect values that are in sliceTwo but not in sliceOne
for _, value := range sliceTwo {
if !slices.Contains(sliceOne, value) {
diffValues = append(diffValues, value)
}
}
return diffValues
}

// converts resourceAliases to SetValue for resourceModels. validates modelAliases contains slugs if not empty
func stringAliasesToSetValue(ctx context.Context, resourceAliases []string, modelAliases basetypes.SetValue) (basetypes.SetValue, diag.Diagnostics) {
aliases := types.SetNull(types.StringType)

// config has `aliases = null` or omitted
if modelAliases.IsNull() {
return aliases, nil
}
// config has `aliases = []` - not null
if len(modelAliases.Elements()) == 0 {
return types.SetValueFrom(ctx, types.StringType, modelAliases)
}

// check if config has 'aliases' set, but is missing needed alias "slugs" from API
aliasesFromModel, diags := SetValueToStringSlice(ctx, modelAliases)
if diags != nil && diags.HasError() {
return aliases, diags
}
aliasesNeededInConfig := diffBetweenStringSlices(resourceAliases, aliasesFromModel)
if len(aliasesNeededInConfig) > 0 {
// setting aliases here blocks an erroneous error. The "Config error" is what matters
diags.AddError("Config error", fmt.Sprintf(`default aliases from API need to be added to config: %s`, aliasesNeededInConfig))
return aliases, diags
}

// aliases is set and contains alias "slugs" from API
return types.SetValueFrom(ctx, types.StringType, resourceAliases)
}

// getDatasourceFilter originally had a "required" bool input parameter - no longer needed
func getDatasourceFilter(validFieldNames []string) schema.SingleNestedBlock {
return schema.SingleNestedBlock{
Expand Down
7 changes: 7 additions & 0 deletions opslevel/import_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ func getTagsFromResource(client *opslevel.Client, resource opslevel.TaggableReso
return tags, diags
}

// hasTagFormat returns true if the given tag is formatted as '<key>:<value>'
func hasTagFormat(tag string) bool {
parts := strings.Split(tag, ":")
return len(parts) == 2 && len(parts[0]) > 0 && len(parts[1]) > 0
}

// isTagValid returns true if the given tag is formatted as '<resource-id>:<tag-id>'
func isTagValid(tag string) bool {
ids := strings.Split(tag, ":")
return len(ids) == 2 && opslevel.IsID(ids[0]) && opslevel.IsID(ids[1])
Expand Down
66 changes: 13 additions & 53 deletions opslevel/resource_opslevel_infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package opslevel
import (
"context"
"fmt"
"slices"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
Expand Down Expand Up @@ -73,16 +72,7 @@ func NewInfrastructureResourceModel(ctx context.Context, infrastructure opslevel
Owner: RequiredStringValue(string(infrastructure.Owner.Id())),
Schema: RequiredStringValue(infrastructure.Schema),
}

if len(infrastructure.Aliases) == 0 && givenModel.Aliases.IsNull() {
infrastructureResourceModel.Aliases = types.SetNull(types.StringType)
} else {
aliases, diags := types.SetValueFrom(ctx, types.StringType, infrastructure.Aliases)
if diags != nil && diags.HasError() {
return InfrastructureResourceModel{}, diags
}
infrastructureResourceModel.Aliases = aliases
}
infrastructureResourceModel.Aliases, diags = stringAliasesToSetValue(ctx, infrastructure.Aliases, givenModel.Aliases)

return infrastructureResourceModel, diags
}
Expand Down Expand Up @@ -173,19 +163,19 @@ func (r *InfrastructureResource) Create(ctx context.Context, req resource.Create
return
}

givenAliases, diags := SetValueToStringSlice(ctx, planModel.Aliases)
if diags != nil && diags.HasError() {
resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given infrastructure aliases: '%s'", planModel.Aliases))
return
}
if err = reconcileInfraAliases(*r.client, givenAliases, infrastructure); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile infrastructure aliases: '%s', got error: %s", givenAliases, err))
return
if len(planModel.Aliases.Elements()) > 0 {
aliases, diags := SetValueToStringSlice(ctx, planModel.Aliases)
if diags != nil && diags.HasError() {
resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given infrastructure aliases: '%s'", planModel.Aliases))
return
}
if err = infrastructure.ReconcileAliases(r.client, aliases); err != nil {
resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("Unable to reconcile infrastructure aliases: '%s'\n%s", aliases, err))
}
}

createdInfrastructureResourceModel, diags := NewInfrastructureResourceModel(ctx, *infrastructure, planModel)
resp.Diagnostics.Append(diags...)
createdInfrastructureResourceModel.Aliases = planModel.Aliases

tflog.Trace(ctx, "created a infrastructure resource")
resp.Diagnostics.Append(resp.State.Set(ctx, &createdInfrastructureResourceModel)...)
Expand All @@ -208,7 +198,6 @@ func (r *InfrastructureResource) Read(ctx context.Context, req resource.ReadRequ
}
readInfrastructureResourceModel, diags := NewInfrastructureResourceModel(ctx, *infrastructure, stateModel)
resp.Diagnostics.Append(diags...)
readInfrastructureResourceModel.Aliases = stateModel.Aliases

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &readInfrastructureResourceModel)...)
Expand Down Expand Up @@ -239,21 +228,18 @@ func (r *InfrastructureResource) Update(ctx context.Context, req resource.Update
resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given infrastructure aliases: '%s'", planModel.Aliases))
return
}
if err = reconcileInfraAliases(*r.client, givenAliases, updatedInfrastructure); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile infrastructure aliases: '%s', got error: %s", givenAliases, err))
return
if err = updatedInfrastructure.ReconcileAliases(r.client, givenAliases); err != nil {
resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("Unable to reconcile infrastructure aliases: '%s'\n%s", givenAliases, err))
}

updatedInfrastructureResourceModel, diags := NewInfrastructureResourceModel(ctx, *updatedInfrastructure, planModel)
resp.Diagnostics.Append(diags...)

if planModel.ProviderData == nil && updatedInfrastructureResourceModel.ProviderData != nil {
resp.Diagnostics.AddError("Known error", "Unable to unset 'provider_data' field for now. We have a planned fix for this.")
return
}

resp.Diagnostics.Append(diags...)
updatedInfrastructureResourceModel.Aliases = planModel.Aliases

tflog.Trace(ctx, "updated a infrastructure resource")
resp.Diagnostics.Append(resp.State.Set(ctx, &updatedInfrastructureResourceModel)...)
}
Expand Down Expand Up @@ -321,29 +307,3 @@ func expandInfraProviderData(providerData InfraProviderData) *opslevel.InfraProv
URL: providerData.Url.ValueString(),
}
}

func reconcileInfraAliases(client opslevel.Client, aliasesFromConfig []string, infra *opslevel.InfrastructureResource) error {
// delete aliases found in infrastructure resource but not listed in Terraform config
for _, alias := range infra.Aliases {
if !slices.Contains(aliasesFromConfig, alias) {
if err := client.DeleteInfraAlias(alias); err != nil {
return err
}
}
}

// create aliases listed in Terraform config but not found in infrastructure resource
newInfraAliases := []string{}
for _, configAlias := range aliasesFromConfig {
if !slices.Contains(infra.Aliases, configAlias) {
newInfraAliases = append(newInfraAliases, configAlias)
}
}
if len(newInfraAliases) > 0 {
if _, err := client.CreateAliases(opslevel.ID(infra.Id), newInfraAliases); err != nil {
return err
}
}
infra.Aliases = aliasesFromConfig
return nil
}
Loading

0 comments on commit b72b08d

Please sign in to comment.