From b72b08dd2fa37d05956dae2a642df7ea100137b1 Mon Sep 17 00:00:00 2001 From: David Bloss Date: Wed, 26 Jun 2024 12:51:13 -0700 Subject: [PATCH] update reconcileAliases to use opslevel-go implementation (#383) * 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 --- .../unreleased/Bugfix-20240626-140311.yaml | 3 + .../unreleased/Feature-20240624-160020.yaml | 3 + .../opslevel_infrastructure/resource.tf | 5 +- .../resources/opslevel_service/resource.tf | 4 +- examples/resources/opslevel_team/resource.tf | 2 +- opslevel/common.go | 51 ++++++++ opslevel/import_utils.go | 7 ++ opslevel/resource_opslevel_infra.go | 66 ++-------- opslevel/resource_opslevel_service.go | 115 ++++-------------- opslevel/resource_opslevel_team.go | 71 ++++------- opslevel/terraform_type_conversions.go | 20 +++ opslevel/validators.go | 5 +- submodules/opslevel-go | 2 +- 13 files changed, 151 insertions(+), 203 deletions(-) create mode 100644 .changes/unreleased/Bugfix-20240626-140311.yaml create mode 100644 .changes/unreleased/Feature-20240624-160020.yaml diff --git a/.changes/unreleased/Bugfix-20240626-140311.yaml b/.changes/unreleased/Bugfix-20240626-140311.yaml new file mode 100644 index 00000000..a59ecf0c --- /dev/null +++ b/.changes/unreleased/Bugfix-20240626-140311.yaml @@ -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 diff --git a/.changes/unreleased/Feature-20240624-160020.yaml b/.changes/unreleased/Feature-20240624-160020.yaml new file mode 100644 index 00000000..e91d6e1a --- /dev/null +++ b/.changes/unreleased/Feature-20240624-160020.yaml @@ -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 diff --git a/examples/resources/opslevel_infrastructure/resource.tf b/examples/resources/opslevel_infrastructure/resource.tf index 3bb648d2..d7fddab6 100644 --- a/examples/resources/opslevel_infrastructure/resource.tf +++ b/examples/resources/opslevel_infrastructure/resource.tf @@ -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" diff --git a/examples/resources/opslevel_service/resource.tf b/examples/resources/opslevel_service/resource.tf index 2d26527d..b952e65f 100644 --- a/examples/resources/opslevel_service/resource.tf +++ b/examples/resources/opslevel_service/resource.tf @@ -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" @@ -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"] } diff --git a/examples/resources/opslevel_team/resource.tf b/examples/resources/opslevel_team/resource.tf index 4a1e4618..09aff0a6 100644 --- a/examples/resources/opslevel_team/resource.tf +++ b/examples/resources/opslevel_team/resource.tf @@ -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 { diff --git a/opslevel/common.go b/opslevel/common.go index 2b8cd766..25b977f0 100644 --- a/opslevel/common.go +++ b/opslevel/common.go @@ -3,6 +3,7 @@ package opslevel import ( "context" "fmt" + "slices" "strconv" "strings" "time" @@ -10,6 +11,7 @@ import ( "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" @@ -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{ diff --git a/opslevel/import_utils.go b/opslevel/import_utils.go index 02c15d87..975a2977 100644 --- a/opslevel/import_utils.go +++ b/opslevel/import_utils.go @@ -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 ':' +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 ':' func isTagValid(tag string) bool { ids := strings.Split(tag, ":") return len(ids) == 2 && opslevel.IsID(ids[0]) && opslevel.IsID(ids[1]) diff --git a/opslevel/resource_opslevel_infra.go b/opslevel/resource_opslevel_infra.go index e47e0b35..94cc16a7 100644 --- a/opslevel/resource_opslevel_infra.go +++ b/opslevel/resource_opslevel_infra.go @@ -3,7 +3,6 @@ package opslevel import ( "context" "fmt" - "slices" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -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 } @@ -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)...) @@ -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)...) @@ -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)...) } @@ -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 -} diff --git a/opslevel/resource_opslevel_service.go b/opslevel/resource_opslevel_service.go index e70f5304..4c280dc5 100644 --- a/opslevel/resource_opslevel_service.go +++ b/opslevel/resource_opslevel_service.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "regexp" - "slices" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -65,13 +64,9 @@ func newServiceResourceModel(ctx context.Context, service opslevel.Service, give TierAlias: OptionalStringValue(service.Tier.Alias), } - if len(service.ManagedAliases) == 0 && givenModel.Aliases.IsNull() { - serviceResourceModel.Aliases = types.SetNull(types.StringType) - } else { - serviceResourceModel.Aliases, diags = types.SetValueFrom(ctx, types.StringType, service.ManagedAliases) - if diags != nil && diags.HasError() { - return ServiceResourceModel{}, diags - } + serviceResourceModel.Aliases, diags = stringAliasesToSetValue(ctx, service.Aliases, givenModel.Aliases) + if diags != nil && diags.HasError() { + return serviceResourceModel, diags } if givenModel.Tags.IsNull() && (service.Tags != nil || len(service.Tags.Nodes) == 0) { @@ -194,13 +189,11 @@ func (r *ServiceResource) Schema(ctx context.Context, req resource.SchemaRequest func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var planModel ServiceResourceModel - - // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { return } + serviceCreateInput := opslevel.ServiceCreateInput{ Description: planModel.Description.ValueStringPointer(), Framework: planModel.Framework.ValueStringPointer(), @@ -228,21 +221,23 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given service aliases: '%s'", planModel.Aliases)) return } - err = reconcileServiceAliases(*r.client, givenAliases, service) - if err != nil { - resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile service aliases: '%s', got error: %s", givenAliases, err)) - return + if len(givenAliases) > 0 { + if err = service.ReconcileAliases(r.client, givenAliases); err != nil { + resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("Unable to reconcile service aliases: '%s'\n%s", givenAliases, err)) + resp.Diagnostics.AddWarning("Config warning", "On create, OpsLevel API creates a new alias for services. If this causes issues, create team with empty 'aliases'. Then update team with 'aliases'") + } } - givenTags, diags := SetValueToStringSlice(ctx, planModel.Tags) + givenTags, diags := TagSetValueToTagSlice(ctx, planModel.Tags) if diags != nil && diags.HasError() { resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given service tags: '%s'", planModel.Tags)) return } - if err = reconcileTags(*r.client, givenTags, service); err != nil { - resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile service tags: '%s', got error: %s", givenTags, err)) + if err = r.client.ReconcileTags(service, givenTags); err != nil { + resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile service tags '%s', got error: %s", givenTags, err)) return } + if planModel.ApiDocumentPath.ValueString() != "" { apiDocPath := planModel.ApiDocumentPath.ValueString() if planModel.PreferredApiDocumentSource.IsNull() { @@ -333,23 +328,23 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest return } - givenAliases, diags := SetValueToStringSlice(ctx, planModel.Aliases) - if diags != nil && diags.HasError() { - resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given service aliases: '%s'", planModel.Aliases)) - return - } - err = reconcileServiceAliases(*r.client, givenAliases, service) - if err != nil { - resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile service aliases '%s', go 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 service aliases: '%s'", planModel.Aliases)) + return + } + if err = service.ReconcileAliases(r.client, aliases); err != nil { + resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("Unable to reconcile service aliases: '%s'\n%s", aliases, err)) + } } - givenTags, diags := SetValueToStringSlice(ctx, planModel.Tags) + givenTags, diags := TagSetValueToTagSlice(ctx, planModel.Tags) if diags != nil && diags.HasError() { resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given service tags: '%s'", planModel.Tags)) return } - if err = reconcileTags(*r.client, givenTags, service); err != nil { + if err = r.client.ReconcileTags(service, givenTags); err != nil { resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to reconcile service tags '%s', got error: %s", givenTags, err)) return } @@ -420,65 +415,3 @@ func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *ServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } - -// Assigns new aliases from terraform config to service and deletes aliases not in config -func reconcileServiceAliases(client opslevel.Client, aliasesFromConfig []string, service *opslevel.Service) error { - // delete service aliases found in service but not listed in Terraform config - for _, managedAlias := range service.ManagedAliases { - if !slices.Contains(aliasesFromConfig, managedAlias) { - if err := client.DeleteServiceAlias(managedAlias); err != nil { - return err - } - } - } - - // create aliases listed in Terraform config but not found in service - newServiceAliases := []string{} - for _, configAlias := range aliasesFromConfig { - if !slices.Contains(service.ManagedAliases, configAlias) { - newServiceAliases = append(newServiceAliases, configAlias) - } - } - if len(newServiceAliases) > 0 { - if _, err := client.CreateAliases(service.Id, newServiceAliases); err != nil { - return err - } - } - service.ManagedAliases = aliasesFromConfig - return nil -} - -// Assigns new tags from terraform config to service and deletes tags not in config -func reconcileTags(client opslevel.Client, tagsFromConfig []string, service *opslevel.Service) error { - // delete service tags found in service but not listed in Terraform config - existingTags := []string{} - for _, tag := range service.Tags.Nodes { - flattenedTag := flattenTag(tag) - existingTags = append(existingTags, flattenedTag) - if !slices.Contains(tagsFromConfig, flattenedTag) { - if err := client.DeleteTag(tag.Id); err != nil { - return err - } - } - } - - // format tags listed in Terraform config but not found in service - tagInput := map[string]string{} - for _, tag := range tagsFromConfig { - parts := strings.Split(tag, ":") - if len(parts) != 2 { - return fmt.Errorf("[%s] invalid tag, should be in format 'key:value' (only a single colon between the key and value, no spaces or special characters)", tag) - } - key := parts[0] - value := parts[1] - tagInput[key] = value - } - // assign tags listed in Terraform config but not found in service - assignedTags, err := client.AssignTags(string(service.Id), tagInput) - if err != nil { - return err - } - - service.Tags.Nodes = assignedTags - return nil -} diff --git a/opslevel/resource_opslevel_team.go b/opslevel/resource_opslevel_team.go index b41a11f1..d2062626 100644 --- a/opslevel/resource_opslevel_team.go +++ b/opslevel/resource_opslevel_team.go @@ -3,7 +3,6 @@ package opslevel import ( "context" "fmt" - "slices" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -58,14 +57,9 @@ func NewTeamResourceModel(ctx context.Context, team opslevel.Team, givenModel Te Responsibilities: OptionalStringValue(team.Responsibilities), } - if len(team.ManagedAliases) == 0 && givenModel.Aliases.IsNull() { - teamResourceModel.Aliases = types.SetNull(types.StringType) - } else { - aliases, diags := types.SetValueFrom(ctx, types.StringType, team.ManagedAliases) - if diags != nil && diags.HasError() { - return TeamResourceModel{}, diags - } - teamResourceModel.Aliases = aliases + teamResourceModel.Aliases, diags = stringAliasesToSetValue(ctx, team.Aliases, givenModel.Aliases) + if diags != nil && diags.HasError() { + return teamResourceModel, diags } if len(givenModel.Member) > 0 && team.Memberships != nil { @@ -149,30 +143,33 @@ func (teamResource *TeamResource) Create(ctx context.Context, req resource.Creat if len(members) > 0 { teamCreateInput.Members = &members } - if planModel.Parent.ValueString() != "" { teamCreateInput.ParentTeam = opslevel.NewIdentifier(planModel.Parent.ValueString()) } + team, err := teamResource.client.CreateTeam(teamCreateInput) if err != nil || team == nil { resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("unable to create team, got error: %s", err)) return } - err = team.Hydrate(teamResource.client) - if err != nil { - resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("unable to hydrate team, got error: %s", err)) - return - } - aliases, diags := SetValueToStringSlice(ctx, planModel.Aliases) - resp.Diagnostics.Append(diags...) - if err = teamResource.reconcileTeamAliases(team, aliases); err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to reconcile aliases, got error: %s", 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 team aliases: '%s'", planModel.Aliases)) + return + } + if err = team.ReconcileAliases(teamResource.client, aliases); err != nil { + resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("warning while reconciling team aliases: '%s'\n%s", aliases, err)) + resp.Diagnostics.AddWarning("Config warning", "On create, OpsLevel API creates a new alias for teams. If this causes issues, create team with empty 'aliases'. Then update team with 'aliases'") + } } createdTeamResourceModel, diags := NewTeamResourceModel(ctx, *team, planModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } // if parent is set, use an ID or alias for this field based on what is currently in the state if opslevel.IsID(planModel.Parent.ValueString()) { @@ -265,11 +262,13 @@ func (teamResource *TeamResource) Update(ctx context.Context, req resource.Updat } aliases, diags := SetValueToStringSlice(ctx, planModel.Aliases) - resp.Diagnostics.Append(diags...) - if err = teamResource.reconcileTeamAliases(updatedTeam, aliases); err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to reconcile aliases, got error: %s", err)) + if diags != nil && diags.HasError() { + resp.Diagnostics.AddError("Config error", fmt.Sprintf("Unable to handle given team aliases: '%s'", planModel.Aliases)) return } + if err = updatedTeam.ReconcileAliases(teamResource.client, aliases); err != nil { + resp.Diagnostics.AddWarning("opslevel client error", fmt.Sprintf("warning while reconciling team aliases: '%s'\n%s", aliases, err)) + } updatedTeamResourceModel, diags := NewTeamResourceModel(ctx, *updatedTeam, planModel) resp.Diagnostics.Append(diags...) @@ -316,29 +315,3 @@ func getMembers(members []TeamMember) ([]opslevel.TeamMembershipUserInput, error } return nil, nil } - -func (teamResource *TeamResource) reconcileTeamAliases(team *opslevel.Team, expectedAliases []string) error { - // get list of existing aliases from OpsLevel - existingAliases := team.ManagedAliases - - // if an existing alias is not supposed to be there, delete it - for _, existingAlias := range existingAliases { - if !slices.Contains(expectedAliases, existingAlias) { - err := teamResource.client.DeleteTeamAlias(existingAlias) - if err != nil { - return err - } - } - } - // if an alias does not exist but is supposed to, create it - for _, expectedAlias := range expectedAliases { - if !slices.Contains(existingAliases, expectedAlias) { - _, err := teamResource.client.CreateAliases(team.Id, []string{expectedAlias}) - if err != nil { - return err - } - } - } - team.ManagedAliases = expectedAliases - return nil -} diff --git a/opslevel/terraform_type_conversions.go b/opslevel/terraform_type_conversions.go index 3a3f3b80..adafc051 100644 --- a/opslevel/terraform_type_conversions.go +++ b/opslevel/terraform_type_conversions.go @@ -94,6 +94,26 @@ func SetValueToStringSlice(ctx context.Context, setValue basetypes.SetValue) ([] return dataAsSlice, diags } +func TagSetValueToTagSlice(ctx context.Context, setValue basetypes.SetValue) ([]opslevel.Tag, diag.Diagnostics) { + tagSlice := []opslevel.Tag{} + if setValue.IsNull() { + return tagSlice, nil + } + tagsAsStringSlice, diags := SetValueToStringSlice(ctx, setValue) + if diags.HasError() { + return tagSlice, diags + } + for _, tag := range tagsAsStringSlice { + if hasTagFormat(tag) { + parts := strings.Split(tag, ":") + tagSlice = append(tagSlice, opslevel.Tag{Key: parts[0], Value: parts[1]}) + } else { + diags.AddWarning("Invalid tag format", tag) + } + } + return tagSlice, diags +} + // Converts a basetypes.MapValue to an opslevel.JSON func MapValueToOpslevelJson(ctx context.Context, mapValue basetypes.MapValue) (opslevel.JSON, diag.Diagnostics) { mapAsJson := opslevel.JSON{} diff --git a/opslevel/validators.go b/opslevel/validators.go index 9a0fa153..9ddfa533 100644 --- a/opslevel/validators.go +++ b/opslevel/validators.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -148,9 +147,7 @@ func (v tagFormatValidator) ValidateSet(ctx context.Context, req validator.SetRe elems := req.ConfigValue.Elements() for _, elem := range elems { - elemAsString := unquote(elem.String()) - parts := strings.Split(elemAsString, ":") - if len(parts) == 2 && len(parts[0]) > 0 && len(parts[1]) > 0 { + if hasTagFormat(unquote(elem.String())) { continue } diff --git a/submodules/opslevel-go b/submodules/opslevel-go index 8016ef2d..732e3a57 160000 --- a/submodules/opslevel-go +++ b/submodules/opslevel-go @@ -1 +1 @@ -Subproject commit 8016ef2dfc61c16bd04913d73eb651794e4d4d1e +Subproject commit 732e3a57db13f7b1afde3aaf724f5bfb9edaa358