From 2ec092528c0038b5264b4530311692ad9b41c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Thu, 25 Jul 2024 15:05:01 +0200 Subject: [PATCH 01/12] add resource wiz_project_cloud_account_link --- internal/provider/provider.go | 1 + internal/provider/resource_project.go | 7 +- .../resource_project_cloud_account_link.go | 655 ++++++++++++++++++ ...esource_project_cloud_account_link_test.go | 179 +++++ 4 files changed, 839 insertions(+), 3 deletions(-) create mode 100644 internal/provider/resource_project_cloud_account_link.go create mode 100644 internal/provider/resource_project_cloud_account_link_test.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5fa812d..8d89428 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -302,6 +302,7 @@ yLyKQXhw2W2Xs0qLeC1etA+jTGDK4UfLeC0SF7FSi8o5LL21L8IzApar2pR/ "wiz_security_framework": resourceWizSecurityFramework(), "wiz_service_account": resourceWizServiceAccount(), "wiz_user": resourceWizUser(), + "wiz_project_cloud_account_link": resourceWizProjectCloudAccountLink(), }, } p.ConfigureContextFunc = configure(version, p) diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go index acfc965..18c452f 100644 --- a/internal/provider/resource_project.go +++ b/internal/provider/resource_project.go @@ -317,9 +317,10 @@ func resourceWizProject() *schema.Resource { }, }, "cloud_account_link": { - Type: schema.TypeSet, - Optional: true, - Description: "Associate the project directly with a cloud account by wiz identifier UID to organize all the subscription resources, issues, and findings within this project.", + Type: schema.TypeSet, + Optional: true, + Description: "Please either use this embedded set or the resource wiz_project_cloud_account_link. " + + "Associate the project directly with a cloud account by wiz identifier UID to organize all the subscription resources, issues, and findings within this project.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go new file mode 100644 index 0000000..9272e23 --- /dev/null +++ b/internal/provider/resource_project_cloud_account_link.go @@ -0,0 +1,655 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "github.com/google/uuid" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "wiz.io/hashicorp/terraform-provider-wiz/internal" + "wiz.io/hashicorp/terraform-provider-wiz/internal/client" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" +) + +type CloudAccountSearchResponse struct { + GraphSearch struct { + Nodes []struct { + Entities []struct { + Id string `json:"id"` + } `json:"entities"` + } `json:"nodes"` + } `json:"graphSearch"` +} + +type SearchForCloudAccountVars struct { + ExternalId string `json:"externalId"` + ProjectId string `json:"projectId"` + Quick bool `json:"quick"` +} + +type PartialProjectWithCloudAccountLinks struct { + Project PartialProject `json:"project"` +} + +type PartialProject struct { + CloudAccountLinks []*wiz.ProjectCloudAccountLink +} + +type UpdateProjectCloudAccountLinks struct { + ID string `json:"id"` + Patch PatchProjectCloudAccountLinks `json:"patch"` +} + +type PatchProjectCloudAccountLinks struct { + CloudAccountLinks []*wiz.ProjectCloudAccountLinkInput `json:"cloudAccountLinks"` +} + +func resourceWizProjectCloudAccountLink() *schema.Resource { + return &schema.Resource{ + Description: "Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. " + + "Link of a Project to a Cloud Account.", + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Unique tf-internal identifier for the project cloud account link", + Computed: true, + }, + "project_id": { + Type: schema.TypeString, + Description: "The Wiz internal identifier of the Wiz project to link the cloud account to", + Required: true, + ForceNew: true, + }, + "cloud_account_id": { + Type: schema.TypeString, + Description: "The Wiz internal identifier for the Cloud Account Subscription.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "external_cloud_account_id": { + Type: schema.TypeString, + Description: "The external identifier for the Cloud Account, e.g. an azure subscription id or an aws account id.", + Optional: true, + Computed: true, + ForceNew: true, + }, + "environment": { + Type: schema.TypeString, + Description: fmt.Sprintf( + "The environment.\n - Allowed values: %s", + utils.SliceOfStringToMDUList( + wiz.Environment, + ), + ), + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice( + wiz.Environment, + false, + ), + ), + Default: "PRODUCTION", + }, + "shared": { + Type: schema.TypeBool, + Description: "Subscriptions that host a few projects can be marked as ‘shared subscriptions’ and resources can be filtered by tags.", + Optional: true, + }, + "resource_groups": { + Type: schema.TypeList, + Optional: true, + Description: "Please provide a list of resource group identifiers for filtering by resource groups. `shared` must be true to define resource_groups.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "resource_tags": { + Type: schema.TypeSet, + Description: "Provide a key and value pair for filtering resources. `shared` must be true to define resource_tags.", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + CreateContext: resourceWizProjectCloudAccountLinkCreate, + ReadContext: resourceWizProjectCloudAccountLinkRead, + UpdateContext: resourceWizProjectCloudAccountLinkUpdate, + DeleteContext: resourceWizProjectCloudAccountLinkDelete, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + // schema for import id: link|| + projectId, cloudAccountId, err := extractIds(d.Id()) + if err != nil { + return nil, err + } + + err = d.Set("project_id", projectId) + if err != nil { + return nil, err + } + + err = d.Set("cloud_account_id", cloudAccountId) + if err != nil { + return nil, err + } + + d.SetId(uuid.NewString()) + + return []*schema.ResourceData{d}, nil + }, + }, + // allow the user to supply both 'cloud_account_id' and 'external_cloud_account_id' + // if none is given, we return and error + // if they do not match, we also return an error + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + cloudAccountId, cloudAccountIdOk := diff.GetOk("cloud_account_id") + externalCloudAccountId, externalCloudAccountIdOk := diff.GetOk("external_cloud_account_id") + if !cloudAccountIdOk && !externalCloudAccountIdOk { + return fmt.Errorf("either cloud_account_id or external_cloud_account_id must be set") + } + + if cloudAccountIdOk && externalCloudAccountIdOk { + queriedAccountId, diags := searchForCloudAccount(ctx, externalCloudAccountId.(string), v) + if len(diags) != 0 { + return fmt.Errorf("error while searching for cloud account in wiz") + } + + if queriedAccountId != cloudAccountId { + return fmt.Errorf("cloud_account_id and external_cloud_account_id must correspond to the same account") + } + } + + return nil + }, + } +} + +func getAccountLinkVar(d *schema.ResourceData, cloudAccountId string) *wiz.ProjectCloudAccountLinkInput { + var localAccount wiz.ProjectCloudAccountLinkInput + + localAccount.Environment = d.Get("environment").(string) + localAccount.CloudAccount = cloudAccountId + localAccount.Shared = utils.ConvertBoolToPointer(d.Get("shared").(bool)) + rgs := utils.ConvertListToString(d.Get("resource_groups").([]interface{})) + if len(rgs) > 0 { + localAccount.ResourceGroups = rgs + } + + // var myResourceTags []*wiz.ResourceTagInput + for _, d := range d.Get("resource_tags").(*schema.Set).List() { + var localResourceTag wiz.ResourceTagInput + for e, f := range d.(map[string]interface{}) { + if e == "key" { + localResourceTag.Key = f.(string) + } + if e == "value" { + localResourceTag.Value = f.(string) + } + } + // myResourceTags = append(myResourceTags, &localResourceTag) + localAccount.ResourceTags = append(localAccount.ResourceTags, &localResourceTag) + } + + return &localAccount +} + +// this is needed, as we query for existing cloud account links, then need +// to send the list with an appended entry back as mutation - but the types are different between +// GET and PATCH. +func accountLinkToAccountLinkInput(link *wiz.ProjectCloudAccountLink) *wiz.ProjectCloudAccountLinkInput { + resourceTagsInput := make([]*wiz.ResourceTagInput, len(link.ResourceTags)) + for i, tag := range link.ResourceTags { + resourceTagsInput[i] = &wiz.ResourceTagInput{ + Key: tag.Key, + Value: tag.Value, + } + } + + return &wiz.ProjectCloudAccountLinkInput{ + CloudAccount: link.CloudAccount.ID, + Environment: link.Environment, + ResourceGroups: link.ResourceGroups, + ResourceTags: resourceTagsInput, + Shared: &link.Shared, + } +} + +func searchForCloudAccount(ctx context.Context, externalId string, m interface{}) (string, diag.Diagnostics) { + tflog.Info(ctx, "searching for account in wiz inventory...") + + readCloudAccountsQuery := `query SearchForCloudAccount($externalId: String!, $projectId: String!, $quick: Boolean) { + graphSearch(query: { + type: [SUBSCRIPTION], + where: { + externalId: { + EQUALS: [$externalId] + } + } + }, + projectId: $projectId, quick: $quick) { + nodes { + entities { + id + } + } + } + }` + + vars := &SearchForCloudAccountVars{ + ExternalId: externalId, + ProjectId: "*", + Quick: true, + } + + respData := &CloudAccountSearchResponse{} + diags := client.ProcessRequest(ctx, m, vars, respData, readCloudAccountsQuery, "SearchForCloudAccount", "read") + if len(diags) > 0 { + return "", diags + + } + + if len(respData.GraphSearch.Nodes) == 0 || len(respData.GraphSearch.Nodes[0].Entities) == 0 { + return "", diag.Errorf("cloud account %s not found in wiz inventory", externalId) + } + + return respData.GraphSearch.Nodes[0].Entities[0].Id, nil +} + +func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizProjectCloudAccountLinkCreate called...") + projectId := d.Get("project_id").(string) + var cloudAccountWizId string + + if v, ok := d.GetOk("cloud_account_id"); ok { + cloudAccountWizId = v.(string) + } else { + cloudAccountUpstreamId := d.Get("external_cloud_account_id").(string) + var diagsSearch diag.Diagnostics + cloudAccountWizId, diagsSearch = searchForCloudAccount(ctx, cloudAccountUpstreamId, m) + if len(diagsSearch) > 0 { + return diagsSearch + } + + } + // verify that the link does not already exist in wiz + // if it does, abort and throw an error, as is standard + // terraform behavior (no overwrite or implicit import). + partialProject := &PartialProjectWithCloudAccountLinks{} + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + if linkExists { + return diag.Errorf("cloud account %s is already linked to project %s", cloudAccountWizId, projectId) + } + + // link not present, add it to the project + newCloudAccountLinksList := make([]*wiz.ProjectCloudAccountLinkInput, len(partialProject.Project.CloudAccountLinks)+1) + for i, link := range partialProject.Project.CloudAccountLinks { + newCloudAccountLinksList[i] = accountLinkToAccountLinkInput(link) + } + newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizId) + + // define the graphql query for adding the link by taking the existing list and appending + // the new entry to it - then patch this property on the wiz project + query := `mutation LinkCloudAccountToProject($input: UpdateProjectInput!) { + updateProject(input: $input) { + project { + id + } + } + }` + + // populate the graphql variables + vars := &UpdateProjectCloudAccountLinks{ + ID: projectId, + Patch: PatchProjectCloudAccountLinks{ + CloudAccountLinks: newCloudAccountLinksList, + }, + } + + // process the request + data := &UpdateProject{} + requestDiags = client.ProcessRequest(ctx, m, vars, data, query, "LinkCloudAccountToProject", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + d.SetId(uuid.NewString()) + err := d.Set("cloud_account_id", cloudAccountWizId) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + return resourceWizProjectCloudAccountLinkRead(ctx, d, m) +} + +func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizProjectCloudAccountLinkRead called...") + + // check the id + if d.Id() == "" { + return nil + } + + // define the graphql query + query := `query project ( + $id: ID + ){ + project( + id: $id + ) { + id + name + isFolder + ancestorProjects { + id + } + description + identifiers + slug + archived + businessUnit + projectOwners { + id + name + email + } + securityChampions { + id + name + email + } + riskProfile { + businessImpact + isActivelyDeveloped + hasAuthentication + hasExposedAPI + isInternetFacing + isCustomerFacing + storesData + sensitiveDataTypes + isRegulated + regulatoryStandards + } + cloudOrganizationLinks { + cloudOrganization { + externalId + id + name + path + } + resourceTags { + key + value + } + resourceGroups + shared + environment + } + cloudAccountLinks { + cloudAccount { + externalId + id + name + } + resourceTags { + key + value + } + resourceGroups + shared + environment + } + kubernetesClustersLinks { + kubernetesCluster { + id + } + environment + namespaces + shared + } + } + }` + + projectId := d.Get("project_id").(string) + cloudAccountWizId := d.Get("cloud_account_id").(string) + + // populate the graphql variables + vars := &internal.QueryVariables{} + vars.ID = projectId + + // process the request + data := &ReadProjectPayload{} + requestDiags := client.ProcessRequest(ctx, m, vars, data, query, "project", "read") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + err := d.Set("project_id", data.Project.ID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + // extract the single cloud account link we want + cloudAccountLink, err := extractCloudAccountLink(data.Project.CloudAccountLinks, cloudAccountWizId) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("cloud_account_id", cloudAccountLink.CloudAccount.ID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("external_cloud_account_id", cloudAccountLink.CloudAccount.ExternalID) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("environment", cloudAccountLink.Environment) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("shared", cloudAccountLink.Shared) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("resource_groups", cloudAccountLink.ResourceGroups) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + err = d.Set("resource_tags", cloudAccountLink.ResourceTags) + if err != nil { + return append(diags, diag.FromErr(err)...) + } + + return diags +} + +func extractIds(id string) (string, string, error) { + parts := strings.Split(id, "|") + if len(parts) != 3 { + return "", "", errors.New("invalid ID format") + } + + return parts[1], parts[2], nil +} + +func extractCloudAccountLink(cloudAccountLinks []*wiz.ProjectCloudAccountLink, wizCloudAccountId string) (*wiz.ProjectCloudAccountLink, error) { + for _, cloudAccountLink := range cloudAccountLinks { + if cloudAccountLink.CloudAccount.ID == wizCloudAccountId { + return cloudAccountLink, nil + } + } + + return nil, fmt.Errorf("cloud account with id %s not found in cloud account links of project", wizCloudAccountId) +} + +func resourceWizProjectCloudAccountLinkUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizProjectCloudAccountLinkUpdate called...") + projectId := d.Get("project_id").(string) + cloudAccountWizId := d.Get("cloud_account_id").(string) + + // verify that the link exists in wiz + partialProject := &PartialProjectWithCloudAccountLinks{} + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + if !linkExists { + return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizId, projectId) + } + + newCloudAccountLinksList := make([]*wiz.ProjectCloudAccountLinkInput, len(partialProject.Project.CloudAccountLinks)+1) + for i, link := range partialProject.Project.CloudAccountLinks { + newCloudAccountLinksList[i] = accountLinkToAccountLinkInput(link) + } + newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizId) + + query := `mutation LinkCloudAccountToProject($input: UpdateProjectInput!) { + updateProject(input: $input) { + project { + id + } + } + }` + + // populate the graphql variables + vars := &UpdateProjectCloudAccountLinks{ + ID: projectId, + Patch: PatchProjectCloudAccountLinks{ + CloudAccountLinks: newCloudAccountLinksList, + }, + } + + // process the request + data := &UpdateProject{} + requestDiags = client.ProcessRequest(ctx, m, vars, data, query, "LinkCloudAccountToProject", "update") + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + return resourceWizProjectCloudAccountLinkRead(ctx, d, m) +} + +func resourceWizProjectCloudAccountLinkDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { + tflog.Info(ctx, "resourceWizProjectCloudAccountLinkDelete called...") + projectId := d.Get("project_id").(string) + cloudAccountWizId := d.Get("cloud_account_id").(string) + + // verify that the link exists in wiz + partialProject := &PartialProjectWithCloudAccountLinks{} + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return diags + } + + if !linkExists { + return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizId, projectId) + } + + newCloudAccountLinksList := make([]*wiz.ProjectCloudAccountLinkInput, 0, len(partialProject.Project.CloudAccountLinks)) + for _, link := range partialProject.Project.CloudAccountLinks { + if link.CloudAccount.ID != cloudAccountWizId { + newCloudAccountLinksList = append(newCloudAccountLinksList, accountLinkToAccountLinkInput(link)) + } + } + + query := `mutation LinkCloudAccountToProject($input: UpdateProjectInput!) { + updateProject(input: $input) { + project { + id + } + } + }` + + // populate the graphql variables + vars := &UpdateProjectCloudAccountLinks{ + ID: projectId, + Patch: PatchProjectCloudAccountLinks{ + CloudAccountLinks: newCloudAccountLinksList, + }, + } + + // process the request + data := &UpdateProject{} + requestDiags = client.ProcessRequest(ctx, m, vars, data, query, "LinkCloudAccountToProject", "update") + diags = append(diags, requestDiags...) + + return diags +} + +func checkCloudAccountLinkExistence(ctx context.Context, m interface{}, projectId string, cloudAccountWizId string, partialProject *PartialProjectWithCloudAccountLinks) (exists bool, diags diag.Diagnostics) { + readExistingLinksQuery := `query project ($id: ID) { + project( + id: $id + ) { + cloudAccountLinks { + cloudAccount { + externalId + id + name + } + resourceTags { + key + value + } + resourceGroups + shared + environment + } + } + }` + + // read existing cloud account links + requestDiags := client.ProcessRequest(ctx, m, + &internal.QueryVariables{ID: projectId}, partialProject, readExistingLinksQuery, + "project_cloud_account_link", "read") + + // handle errors from read + diags = append(diags, requestDiags...) + if len(diags) > 0 { + return false, diags + } + + // check if desired link exists + linkExists := slices.ContainsFunc( + partialProject.Project.CloudAccountLinks, + func(link *wiz.ProjectCloudAccountLink) bool { + return link.CloudAccount.ID == cloudAccountWizId + }, + ) + + return linkExists, diags +} diff --git a/internal/provider/resource_project_cloud_account_link_test.go b/internal/provider/resource_project_cloud_account_link_test.go new file mode 100644 index 0000000..4219367 --- /dev/null +++ b/internal/provider/resource_project_cloud_account_link_test.go @@ -0,0 +1,179 @@ +package provider + +import ( + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "reflect" + "testing" + "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" + "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" +) + +const ( + environmentProd = "PRODUCTION" + environmentDev = "DEVELOPMENT" + tag1 = "tag1" + value1 = "value1" +) + +var cloudAccountId = uuid.NewString() +var resourceGroups = []string{"group1", "group2"} +var resourceGroupsInterface = []interface{}{"group1", "group2"} + +func TestGetAccountLinkVar(t *testing.T) { + expected := &wiz.ProjectCloudAccountLinkInput{ + CloudAccount: cloudAccountId, + Environment: environmentProd, + Shared: utils.ConvertBoolToPointer(true), + ResourceGroups: resourceGroups, + ResourceTags: []*wiz.ResourceTagInput{ + { + Key: tag1, + Value: value1, + }, + }, + } + + d := schema.TestResourceDataRaw( + t, + resourceWizProjectCloudAccountLink().Schema, + map[string]interface{}{ + "cloud_account_id": cloudAccountId, + "environment": environmentProd, + "shared": true, + "resource_groups": resourceGroupsInterface, + "resource_tags": []interface{}{ + map[string]interface{}{ + "key": tag1, + "value": value1, + }, + }, + }, + ) + + accountLink := getAccountLinkVar(d, cloudAccountId) + if !reflect.DeepEqual(expected, accountLink) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + utils.PrettyPrint(accountLink), + utils.PrettyPrint(expected), + ) + } +} + +func TestAccountLinkToAccountLinkInput(t *testing.T) { + link := &wiz.ProjectCloudAccountLink{ + CloudAccount: wiz.CloudAccount{ + ID: cloudAccountId, + }, + Environment: environmentProd, + ResourceTags: []*wiz.ResourceTag{ + { + Key: tag1, + Value: value1, + }, + }, + Shared: true, + } + + expected := &wiz.ProjectCloudAccountLinkInput{ + CloudAccount: cloudAccountId, + Environment: environmentProd, + ResourceTags: []*wiz.ResourceTagInput{ + { + Key: tag1, + Value: value1, + }, + }, + Shared: utils.ConvertBoolToPointer(true), + } + + result := accountLinkToAccountLinkInput(link) + if !reflect.DeepEqual(expected, result) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + utils.PrettyPrint(result), + utils.PrettyPrint(expected), + ) + } +} + +func TestExtractCloudAccountLink(t *testing.T) { + cloudAccountLinks := []*wiz.ProjectCloudAccountLink{ + { + CloudAccount: wiz.CloudAccount{ + ID: cloudAccountId, + }, + Environment: environmentProd, + Shared: true, + }, + { + CloudAccount: wiz.CloudAccount{ + ID: "other-id", + }, + Environment: environmentDev, + Shared: false, + }, + } + + expected := &wiz.ProjectCloudAccountLink{ + CloudAccount: wiz.CloudAccount{ + ID: cloudAccountId, + }, + Environment: environmentProd, + Shared: true, + } + + result, err := extractCloudAccountLink(cloudAccountLinks, cloudAccountId) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !reflect.DeepEqual(expected, result) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + utils.PrettyPrint(result), + utils.PrettyPrint(expected), + ) + } +} + +func TestExtractIds(t *testing.T) { + testCases := []struct { + name string + input string + expectedProj string + expectedCloud string + expectErr bool + }{ + { + name: "Valid ID", + input: "link|projectId|cloudAccountUpstreamId", + expectedProj: "projectId", + expectedCloud: "cloudAccountUpstreamId", + expectErr: false, + }, + { + name: "Invalid ID", + input: "invalidId", + expectedProj: "", + expectedCloud: "", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + projId, cloudId, err := extractIds(tc.input) + if (err != nil) != tc.expectErr { + t.Errorf("Expected error: %v, got: %v", tc.expectErr, err) + } + if projId != tc.expectedProj { + t.Errorf("Expected project ID: %s, got: %s", tc.expectedProj, projId) + } + if cloudId != tc.expectedCloud { + t.Errorf("Expected cloud ID: %s, got: %s", tc.expectedCloud, cloudId) + } + }) + } +} From 4956c12bcec10f242eccc131b2b8ee14428e1dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Thu, 25 Jul 2024 15:05:13 +0200 Subject: [PATCH 02/12] add examples --- .../wiz_project_cloud_account_link/import.sh | 2 ++ .../resource.tf | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 examples/resources/wiz_project_cloud_account_link/import.sh create mode 100644 examples/resources/wiz_project_cloud_account_link/resource.tf diff --git a/examples/resources/wiz_project_cloud_account_link/import.sh b/examples/resources/wiz_project_cloud_account_link/import.sh new file mode 100644 index 0000000..1fd1c41 --- /dev/null +++ b/examples/resources/wiz_project_cloud_account_link/import.sh @@ -0,0 +1,2 @@ +# The id for importing a wiz_project_cloud_account_link has to be in this format: 'link||' +terraform import wiz_project_cloud_account_link.example_import "link|ee25cc95-82b0-4543-8934-5bc655b86786|5cc3a684-44cb-4cd5-b78f-f029c25dc617" \ No newline at end of file diff --git a/examples/resources/wiz_project_cloud_account_link/resource.tf b/examples/resources/wiz_project_cloud_account_link/resource.tf new file mode 100644 index 0000000..df2203a --- /dev/null +++ b/examples/resources/wiz_project_cloud_account_link/resource.tf @@ -0,0 +1,21 @@ +# A link from a project to a cloud account can be created using the accounts id in wiz +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" + environment = "PRODUCTION" +} + +# Or using the external id of the cloud account +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" + environment = "PRODUCTION" +} + +# Both can be supplied but they have to belong to the same account +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" + external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" + environment = "PRODUCTION" +} From 348e8fbb6ea13d819508090b180ea36b8c2348f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Fri, 26 Jul 2024 10:04:07 +0200 Subject: [PATCH 03/12] add comments to types + rename Ids --- .../resource_project_cloud_account_link.go | 110 ++++++++++-------- ...esource_project_cloud_account_link_test.go | 28 ++--- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index 9272e23..52a74b1 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -19,6 +19,8 @@ import ( "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" ) +// CloudAccountSearchResponse represents the response from a cloud account search. +// It includes a GraphSearch object that contains a list of Nodes, each with a list of Entities. type CloudAccountSearchResponse struct { GraphSearch struct { Nodes []struct { @@ -29,25 +31,35 @@ type CloudAccountSearchResponse struct { } `json:"graphSearch"` } +// SearchForCloudAccountVars represents the variables for a cloud account search. +// It includes the external ID, project ID, and a quick search flag. type SearchForCloudAccountVars struct { - ExternalId string `json:"externalId"` - ProjectId string `json:"projectId"` + ExternalID string `json:"externalId"` + ProjectID string `json:"projectId"` Quick bool `json:"quick"` } +// PartialProjectWithCloudAccountLinks represents a partial project with cloud account links. +// It includes a Project object. type PartialProjectWithCloudAccountLinks struct { Project PartialProject `json:"project"` } +// PartialProject represents a partial project. +// It includes a list of cloud account links. type PartialProject struct { CloudAccountLinks []*wiz.ProjectCloudAccountLink } +// UpdateProjectCloudAccountLinks represents the input for updating project cloud account links. +// It includes the ID and a Patch object. type UpdateProjectCloudAccountLinks struct { ID string `json:"id"` Patch PatchProjectCloudAccountLinks `json:"patch"` } +// PatchProjectCloudAccountLinks represents the patch object for updating project cloud account links. +// It includes a list of wiz.ProjectCloudAccountLinkInput. type PatchProjectCloudAccountLinks struct { CloudAccountLinks []*wiz.ProjectCloudAccountLinkInput `json:"cloudAccountLinks"` } @@ -137,17 +149,17 @@ func resourceWizProjectCloudAccountLink() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { // schema for import id: link|| - projectId, cloudAccountId, err := extractIds(d.Id()) + projectID, cloudAccountID, err := extractIDs(d.Id()) if err != nil { return nil, err } - err = d.Set("project_id", projectId) + err = d.Set("project_id", projectID) if err != nil { return nil, err } - err = d.Set("cloud_account_id", cloudAccountId) + err = d.Set("cloud_account_id", cloudAccountID) if err != nil { return nil, err } @@ -161,19 +173,19 @@ func resourceWizProjectCloudAccountLink() *schema.Resource { // if none is given, we return and error // if they do not match, we also return an error CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - cloudAccountId, cloudAccountIdOk := diff.GetOk("cloud_account_id") - externalCloudAccountId, externalCloudAccountIdOk := diff.GetOk("external_cloud_account_id") - if !cloudAccountIdOk && !externalCloudAccountIdOk { + cloudAccountID, cloudAccountIDOk := diff.GetOk("cloud_account_id") + externalCloudAccountID, externalCloudAccountIDOk := diff.GetOk("external_cloud_account_id") + if !cloudAccountIDOk && !externalCloudAccountIDOk { return fmt.Errorf("either cloud_account_id or external_cloud_account_id must be set") } - if cloudAccountIdOk && externalCloudAccountIdOk { - queriedAccountId, diags := searchForCloudAccount(ctx, externalCloudAccountId.(string), v) + if cloudAccountIDOk && externalCloudAccountIDOk { + queriedAccountID, diags := searchForCloudAccount(ctx, externalCloudAccountID.(string), v) if len(diags) != 0 { return fmt.Errorf("error while searching for cloud account in wiz") } - if queriedAccountId != cloudAccountId { + if queriedAccountID != cloudAccountID { return fmt.Errorf("cloud_account_id and external_cloud_account_id must correspond to the same account") } } @@ -183,11 +195,11 @@ func resourceWizProjectCloudAccountLink() *schema.Resource { } } -func getAccountLinkVar(d *schema.ResourceData, cloudAccountId string) *wiz.ProjectCloudAccountLinkInput { +func getAccountLinkVar(d *schema.ResourceData, cloudAccountID string) *wiz.ProjectCloudAccountLinkInput { var localAccount wiz.ProjectCloudAccountLinkInput localAccount.Environment = d.Get("environment").(string) - localAccount.CloudAccount = cloudAccountId + localAccount.CloudAccount = cloudAccountID localAccount.Shared = utils.ConvertBoolToPointer(d.Get("shared").(bool)) rgs := utils.ConvertListToString(d.Get("resource_groups").([]interface{})) if len(rgs) > 0 { @@ -233,7 +245,7 @@ func accountLinkToAccountLinkInput(link *wiz.ProjectCloudAccountLink) *wiz.Proje } } -func searchForCloudAccount(ctx context.Context, externalId string, m interface{}) (string, diag.Diagnostics) { +func searchForCloudAccount(ctx context.Context, externalID string, m interface{}) (string, diag.Diagnostics) { tflog.Info(ctx, "searching for account in wiz inventory...") readCloudAccountsQuery := `query SearchForCloudAccount($externalId: String!, $projectId: String!, $quick: Boolean) { @@ -255,8 +267,8 @@ func searchForCloudAccount(ctx context.Context, externalId string, m interface{} }` vars := &SearchForCloudAccountVars{ - ExternalId: externalId, - ProjectId: "*", + ExternalID: externalID, + ProjectID: "*", Quick: true, } @@ -268,7 +280,7 @@ func searchForCloudAccount(ctx context.Context, externalId string, m interface{} } if len(respData.GraphSearch.Nodes) == 0 || len(respData.GraphSearch.Nodes[0].Entities) == 0 { - return "", diag.Errorf("cloud account %s not found in wiz inventory", externalId) + return "", diag.Errorf("cloud account %s not found in wiz inventory", externalID) } return respData.GraphSearch.Nodes[0].Entities[0].Id, nil @@ -276,15 +288,15 @@ func searchForCloudAccount(ctx context.Context, externalId string, m interface{} func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { tflog.Info(ctx, "resourceWizProjectCloudAccountLinkCreate called...") - projectId := d.Get("project_id").(string) - var cloudAccountWizId string + projectID := d.Get("project_id").(string) + var cloudAccountWizID string if v, ok := d.GetOk("cloud_account_id"); ok { - cloudAccountWizId = v.(string) + cloudAccountWizID = v.(string) } else { cloudAccountUpstreamId := d.Get("external_cloud_account_id").(string) var diagsSearch diag.Diagnostics - cloudAccountWizId, diagsSearch = searchForCloudAccount(ctx, cloudAccountUpstreamId, m) + cloudAccountWizID, diagsSearch = searchForCloudAccount(ctx, cloudAccountUpstreamId, m) if len(diagsSearch) > 0 { return diagsSearch } @@ -294,14 +306,14 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res // if it does, abort and throw an error, as is standard // terraform behavior (no overwrite or implicit import). partialProject := &PartialProjectWithCloudAccountLinks{} - linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectID, cloudAccountWizID, partialProject) diags = append(diags, requestDiags...) if len(diags) > 0 { return diags } if linkExists { - return diag.Errorf("cloud account %s is already linked to project %s", cloudAccountWizId, projectId) + return diag.Errorf("cloud account %s is already linked to project %s", cloudAccountWizID, projectID) } // link not present, add it to the project @@ -309,7 +321,7 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res for i, link := range partialProject.Project.CloudAccountLinks { newCloudAccountLinksList[i] = accountLinkToAccountLinkInput(link) } - newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizId) + newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizID) // define the graphql query for adding the link by taking the existing list and appending // the new entry to it - then patch this property on the wiz project @@ -323,7 +335,7 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res // populate the graphql variables vars := &UpdateProjectCloudAccountLinks{ - ID: projectId, + ID: projectID, Patch: PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, @@ -338,7 +350,7 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res } d.SetId(uuid.NewString()) - err := d.Set("cloud_account_id", cloudAccountWizId) + err := d.Set("cloud_account_id", cloudAccountWizID) if err != nil { return append(diags, diag.FromErr(err)...) } @@ -434,12 +446,12 @@ func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.Resou } }` - projectId := d.Get("project_id").(string) - cloudAccountWizId := d.Get("cloud_account_id").(string) + projectID := d.Get("project_id").(string) + cloudAccountWizID := d.Get("cloud_account_id").(string) // populate the graphql variables vars := &internal.QueryVariables{} - vars.ID = projectId + vars.ID = projectID // process the request data := &ReadProjectPayload{} @@ -455,7 +467,7 @@ func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.Resou } // extract the single cloud account link we want - cloudAccountLink, err := extractCloudAccountLink(data.Project.CloudAccountLinks, cloudAccountWizId) + cloudAccountLink, err := extractCloudAccountLink(data.Project.CloudAccountLinks, cloudAccountWizID) if err != nil { return append(diags, diag.FromErr(err)...) } @@ -493,7 +505,7 @@ func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.Resou return diags } -func extractIds(id string) (string, string, error) { +func extractIDs(id string) (string, string, error) { parts := strings.Split(id, "|") if len(parts) != 3 { return "", "", errors.New("invalid ID format") @@ -502,38 +514,38 @@ func extractIds(id string) (string, string, error) { return parts[1], parts[2], nil } -func extractCloudAccountLink(cloudAccountLinks []*wiz.ProjectCloudAccountLink, wizCloudAccountId string) (*wiz.ProjectCloudAccountLink, error) { +func extractCloudAccountLink(cloudAccountLinks []*wiz.ProjectCloudAccountLink, wizCloudAccountID string) (*wiz.ProjectCloudAccountLink, error) { for _, cloudAccountLink := range cloudAccountLinks { - if cloudAccountLink.CloudAccount.ID == wizCloudAccountId { + if cloudAccountLink.CloudAccount.ID == wizCloudAccountID { return cloudAccountLink, nil } } - return nil, fmt.Errorf("cloud account with id %s not found in cloud account links of project", wizCloudAccountId) + return nil, fmt.Errorf("cloud account with id %s not found in cloud account links of project", wizCloudAccountID) } func resourceWizProjectCloudAccountLinkUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { tflog.Info(ctx, "resourceWizProjectCloudAccountLinkUpdate called...") - projectId := d.Get("project_id").(string) - cloudAccountWizId := d.Get("cloud_account_id").(string) + projectID := d.Get("project_id").(string) + cloudAccountWizID := d.Get("cloud_account_id").(string) // verify that the link exists in wiz partialProject := &PartialProjectWithCloudAccountLinks{} - linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectID, cloudAccountWizID, partialProject) diags = append(diags, requestDiags...) if len(diags) > 0 { return diags } if !linkExists { - return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizId, projectId) + return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizID, projectID) } newCloudAccountLinksList := make([]*wiz.ProjectCloudAccountLinkInput, len(partialProject.Project.CloudAccountLinks)+1) for i, link := range partialProject.Project.CloudAccountLinks { newCloudAccountLinksList[i] = accountLinkToAccountLinkInput(link) } - newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizId) + newCloudAccountLinksList[len(newCloudAccountLinksList)-1] = getAccountLinkVar(d, cloudAccountWizID) query := `mutation LinkCloudAccountToProject($input: UpdateProjectInput!) { updateProject(input: $input) { @@ -545,7 +557,7 @@ func resourceWizProjectCloudAccountLinkUpdate(ctx context.Context, d *schema.Res // populate the graphql variables vars := &UpdateProjectCloudAccountLinks{ - ID: projectId, + ID: projectID, Patch: PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, @@ -564,24 +576,24 @@ func resourceWizProjectCloudAccountLinkUpdate(ctx context.Context, d *schema.Res func resourceWizProjectCloudAccountLinkDelete(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { tflog.Info(ctx, "resourceWizProjectCloudAccountLinkDelete called...") - projectId := d.Get("project_id").(string) - cloudAccountWizId := d.Get("cloud_account_id").(string) + projectID := d.Get("project_id").(string) + cloudAccountWizID := d.Get("cloud_account_id").(string) // verify that the link exists in wiz partialProject := &PartialProjectWithCloudAccountLinks{} - linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectId, cloudAccountWizId, partialProject) + linkExists, requestDiags := checkCloudAccountLinkExistence(ctx, m, projectID, cloudAccountWizID, partialProject) diags = append(diags, requestDiags...) if len(diags) > 0 { return diags } if !linkExists { - return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizId, projectId) + return diag.Errorf("cloud account with id %s not found in cloud account links of project %s", cloudAccountWizID, projectID) } newCloudAccountLinksList := make([]*wiz.ProjectCloudAccountLinkInput, 0, len(partialProject.Project.CloudAccountLinks)) for _, link := range partialProject.Project.CloudAccountLinks { - if link.CloudAccount.ID != cloudAccountWizId { + if link.CloudAccount.ID != cloudAccountWizID { newCloudAccountLinksList = append(newCloudAccountLinksList, accountLinkToAccountLinkInput(link)) } } @@ -596,7 +608,7 @@ func resourceWizProjectCloudAccountLinkDelete(ctx context.Context, d *schema.Res // populate the graphql variables vars := &UpdateProjectCloudAccountLinks{ - ID: projectId, + ID: projectID, Patch: PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, @@ -610,7 +622,7 @@ func resourceWizProjectCloudAccountLinkDelete(ctx context.Context, d *schema.Res return diags } -func checkCloudAccountLinkExistence(ctx context.Context, m interface{}, projectId string, cloudAccountWizId string, partialProject *PartialProjectWithCloudAccountLinks) (exists bool, diags diag.Diagnostics) { +func checkCloudAccountLinkExistence(ctx context.Context, m interface{}, projectID string, cloudAccountWizID string, partialProject *PartialProjectWithCloudAccountLinks) (exists bool, diags diag.Diagnostics) { readExistingLinksQuery := `query project ($id: ID) { project( id: $id @@ -634,7 +646,7 @@ func checkCloudAccountLinkExistence(ctx context.Context, m interface{}, projectI // read existing cloud account links requestDiags := client.ProcessRequest(ctx, m, - &internal.QueryVariables{ID: projectId}, partialProject, readExistingLinksQuery, + &internal.QueryVariables{ID: projectID}, partialProject, readExistingLinksQuery, "project_cloud_account_link", "read") // handle errors from read @@ -647,7 +659,7 @@ func checkCloudAccountLinkExistence(ctx context.Context, m interface{}, projectI linkExists := slices.ContainsFunc( partialProject.Project.CloudAccountLinks, func(link *wiz.ProjectCloudAccountLink) bool { - return link.CloudAccount.ID == cloudAccountWizId + return link.CloudAccount.ID == cloudAccountWizID }, ) diff --git a/internal/provider/resource_project_cloud_account_link_test.go b/internal/provider/resource_project_cloud_account_link_test.go index 4219367..c789219 100644 --- a/internal/provider/resource_project_cloud_account_link_test.go +++ b/internal/provider/resource_project_cloud_account_link_test.go @@ -16,13 +16,13 @@ const ( value1 = "value1" ) -var cloudAccountId = uuid.NewString() +var cloudAccountID = uuid.NewString() var resourceGroups = []string{"group1", "group2"} var resourceGroupsInterface = []interface{}{"group1", "group2"} func TestGetAccountLinkVar(t *testing.T) { expected := &wiz.ProjectCloudAccountLinkInput{ - CloudAccount: cloudAccountId, + CloudAccount: cloudAccountID, Environment: environmentProd, Shared: utils.ConvertBoolToPointer(true), ResourceGroups: resourceGroups, @@ -38,7 +38,7 @@ func TestGetAccountLinkVar(t *testing.T) { t, resourceWizProjectCloudAccountLink().Schema, map[string]interface{}{ - "cloud_account_id": cloudAccountId, + "cloud_account_id": cloudAccountID, "environment": environmentProd, "shared": true, "resource_groups": resourceGroupsInterface, @@ -51,7 +51,7 @@ func TestGetAccountLinkVar(t *testing.T) { }, ) - accountLink := getAccountLinkVar(d, cloudAccountId) + accountLink := getAccountLinkVar(d, cloudAccountID) if !reflect.DeepEqual(expected, accountLink) { t.Fatalf( "Got:\n\n%#v\n\nExpected:\n\n%#v\n", @@ -64,7 +64,7 @@ func TestGetAccountLinkVar(t *testing.T) { func TestAccountLinkToAccountLinkInput(t *testing.T) { link := &wiz.ProjectCloudAccountLink{ CloudAccount: wiz.CloudAccount{ - ID: cloudAccountId, + ID: cloudAccountID, }, Environment: environmentProd, ResourceTags: []*wiz.ResourceTag{ @@ -77,7 +77,7 @@ func TestAccountLinkToAccountLinkInput(t *testing.T) { } expected := &wiz.ProjectCloudAccountLinkInput{ - CloudAccount: cloudAccountId, + CloudAccount: cloudAccountID, Environment: environmentProd, ResourceTags: []*wiz.ResourceTagInput{ { @@ -102,7 +102,7 @@ func TestExtractCloudAccountLink(t *testing.T) { cloudAccountLinks := []*wiz.ProjectCloudAccountLink{ { CloudAccount: wiz.CloudAccount{ - ID: cloudAccountId, + ID: cloudAccountID, }, Environment: environmentProd, Shared: true, @@ -118,13 +118,13 @@ func TestExtractCloudAccountLink(t *testing.T) { expected := &wiz.ProjectCloudAccountLink{ CloudAccount: wiz.CloudAccount{ - ID: cloudAccountId, + ID: cloudAccountID, }, Environment: environmentProd, Shared: true, } - result, err := extractCloudAccountLink(cloudAccountLinks, cloudAccountId) + result, err := extractCloudAccountLink(cloudAccountLinks, cloudAccountID) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -164,15 +164,15 @@ func TestExtractIds(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - projId, cloudId, err := extractIds(tc.input) + projID, cloudID, err := extractIDs(tc.input) if (err != nil) != tc.expectErr { t.Errorf("Expected error: %v, got: %v", tc.expectErr, err) } - if projId != tc.expectedProj { - t.Errorf("Expected project ID: %s, got: %s", tc.expectedProj, projId) + if projID != tc.expectedProj { + t.Errorf("Expected project ID: %s, got: %s", tc.expectedProj, projID) } - if cloudId != tc.expectedCloud { - t.Errorf("Expected cloud ID: %s, got: %s", tc.expectedCloud, cloudId) + if cloudID != tc.expectedCloud { + t.Errorf("Expected cloud ID: %s, got: %s", tc.expectedCloud, cloudID) } }) } From d734ac737b2c6ca374beeb925f669cdac4a56506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Fri, 26 Jul 2024 10:06:02 +0200 Subject: [PATCH 04/12] correct operation name for create --- internal/provider/resource_project_cloud_account_link.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index 52a74b1..faa5665 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -343,7 +343,7 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res // process the request data := &UpdateProject{} - requestDiags = client.ProcessRequest(ctx, m, vars, data, query, "LinkCloudAccountToProject", "update") + requestDiags = client.ProcessRequest(ctx, m, vars, data, query, "LinkCloudAccountToProject", "create") diags = append(diags, requestDiags...) if len(diags) > 0 { return diags From f940410d4020196d3a42a495aa7aacf16d281c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Fri, 26 Jul 2024 10:11:54 +0200 Subject: [PATCH 05/12] run go generate --- docs/resources/project.md | 2 +- docs/resources/project_cloud_account_link.md | 82 +++++++++++++++++++ .../resource.tf | 14 ++-- 3 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 docs/resources/project_cloud_account_link.md diff --git a/docs/resources/project.md b/docs/resources/project.md index 40fd448..1b9a9a8 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -123,7 +123,7 @@ resource "wiz_project" "test" { - `archived` (Boolean) Whether the project is archived/inactive - Defaults to `false`. - `business_unit` (String) The business unit to which the project belongs. -- `cloud_account_link` (Block Set) Associate the project directly with a cloud account by wiz identifier UID to organize all the subscription resources, issues, and findings within this project. (see [below for nested schema](#nestedblock--cloud_account_link)) +- `cloud_account_link` (Block Set) Please either use this embedded set or the resource wiz_project_cloud_account_link. Associate the project directly with a cloud account by wiz identifier UID to organize all the subscription resources, issues, and findings within this project. (see [below for nested schema](#nestedblock--cloud_account_link)) - `cloud_organization_link` (Block Set) Associate the project with an organizational link to organize all the subscription resources, issues, and findings within this project. (see [below for nested schema](#nestedblock--cloud_organization_link)) - `description` (String) The project description. - `identifiers` (List of String) Identifiers for the project. diff --git a/docs/resources/project_cloud_account_link.md b/docs/resources/project_cloud_account_link.md new file mode 100644 index 0000000..e1a1e68 --- /dev/null +++ b/docs/resources/project_cloud_account_link.md @@ -0,0 +1,82 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "wiz_project_cloud_account_link Resource - terraform-provider-wiz" +subcategory: "" +description: |- + Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. Link of a Project to a Cloud Account. +--- + +# wiz_project_cloud_account_link (Resource) + +Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. Link of a Project to a Cloud Account. + +## Example Usage + +```terraform +# A link from a project to a cloud account can be created using the accounts id in wiz +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" + environment = "PRODUCTION" +} + +# Or using the external id of the cloud account +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" + environment = "PRODUCTION" +} + +# Both can be supplied but they have to belong to the same account +resource "wiz_project_cloud_account_link" "example" { + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" + external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" + environment = "PRODUCTION" +} +``` + + +## Schema + +### Required + +- `project_id` (String) The Wiz internal identifier of the Wiz project to link the cloud account to + +### Optional + +- `cloud_account_id` (String) The Wiz internal identifier for the Cloud Account Subscription. +- `environment` (String) The environment. + - Allowed values: + - PRODUCTION + - STAGING + - DEVELOPMENT + - TESTING + - OTHER + + - Defaults to `PRODUCTION`. +- `external_cloud_account_id` (String) The external identifier for the Cloud Account, e.g. an azure subscription id or an aws account id. +- `resource_groups` (List of String) Please provide a list of resource group identifiers for filtering by resource groups. `shared` must be true to define resource_groups. +- `resource_tags` (Block Set) Provide a key and value pair for filtering resources. `shared` must be true to define resource_tags. (see [below for nested schema](#nestedblock--resource_tags)) +- `shared` (Boolean) Subscriptions that host a few projects can be marked as ‘shared subscriptions’ and resources can be filtered by tags. + +### Read-Only + +- `id` (String) Unique tf-internal identifier for the project cloud account link + + +### Nested Schema for `resource_tags` + +Required: + +- `key` (String) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# The id for importing a wiz_project_cloud_account_link has to be in this format: 'link||' +terraform import wiz_project_cloud_account_link.example_import "link|ee25cc95-82b0-4543-8934-5bc655b86786|5cc3a684-44cb-4cd5-b78f-f029c25dc617" +``` diff --git a/examples/resources/wiz_project_cloud_account_link/resource.tf b/examples/resources/wiz_project_cloud_account_link/resource.tf index df2203a..571dbb7 100644 --- a/examples/resources/wiz_project_cloud_account_link/resource.tf +++ b/examples/resources/wiz_project_cloud_account_link/resource.tf @@ -1,21 +1,21 @@ # A link from a project to a cloud account can be created using the accounts id in wiz resource "wiz_project_cloud_account_link" "example" { - project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" - environment = "PRODUCTION" + environment = "PRODUCTION" } # Or using the external id of the cloud account resource "wiz_project_cloud_account_link" "example" { - project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" - environment = "PRODUCTION" + environment = "PRODUCTION" } # Both can be supplied but they have to belong to the same account resource "wiz_project_cloud_account_link" "example" { - project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" - cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" + project_id = "ee25cc95-82b0-4543-8934-5bc655b86786" + cloud_account_id = "5cc3a684-44cb-4cd5-b78f-f029c25dc617" external_cloud_account_id = "04e56587-4408-402a-9c8c-f454ed45da65" - environment = "PRODUCTION" + environment = "PRODUCTION" } From 18e7a44a5cc7200be19738ceb2ff252a13a27ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Fri, 26 Jul 2024 10:24:08 +0200 Subject: [PATCH 06/12] add acceptance test --- internal/acceptance/common.go | 2 + internal/acceptance/provider_test.go | 2 + ...rce_wiz_project_cloud_account_link_test.go | 45 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 internal/acceptance/resource_wiz_project_cloud_account_link_test.go diff --git a/internal/acceptance/common.go b/internal/acceptance/common.go index 1c8c305..859a606 100644 --- a/internal/acceptance/common.go +++ b/internal/acceptance/common.go @@ -25,4 +25,6 @@ const ( TcReportGraphQuery TestCase = "REPORT_GRAPH_QUERY" // TcCloudConfigRule test case TcCloudConfigRule TestCase = "CLOUD_CONFIG_RULE" + + TcProjectCloudAccountLink = "PROJECT_CLOUD_ACCOUNT_LINK" ) diff --git a/internal/acceptance/provider_test.go b/internal/acceptance/provider_test.go index 551526e..6d1f965 100644 --- a/internal/acceptance/provider_test.go +++ b/internal/acceptance/provider_test.go @@ -47,6 +47,8 @@ func testAccPreCheck(t *testing.T, tc TestCase) { envVars = append(commonEnvVars, "WIZ_SUBSCRIPTION_ID") case TcReportGraphQuery: envVars = append(commonEnvVars, "WIZ_PROJECT_ID") + case TcProjectCloudAccountLink: + envVars = append(commonEnvVars, "WIZ_PROJECT_ID", "WIZ_SUBSCRIPTION_ID") default: t.Fatalf("unknown testCase: %s", tc) } diff --git a/internal/acceptance/resource_wiz_project_cloud_account_link_test.go b/internal/acceptance/resource_wiz_project_cloud_account_link_test.go new file mode 100644 index 0000000..12c048b --- /dev/null +++ b/internal/acceptance/resource_wiz_project_cloud_account_link_test.go @@ -0,0 +1,45 @@ +package acceptance + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceWizProjectCloudAccountLink_basic(t *testing.T) { + projectID := os.Getenv("WIZ_PROJECT_ID") + cloudAccountID := os.Getenv("WIZ_SUBSCRIPTION_ID") + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, TcProjectCloudAccountLink) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testResourceWizProjectCloudAccountLinkBasic(projectID, cloudAccountID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "wiz_project_cloud_account_link.foo", + "project_id", + projectID, + ), + resource.TestCheckResourceAttr( + "wiz_project_cloud_account_link.foo", + "cloud_account_id", + cloudAccountID, + ), + ), + }, + }, + }) +} + +func testResourceWizProjectCloudAccountLinkBasic(projectID string, cloudAccountID string) string { + return fmt.Sprintf(` +resource "wiz_project_cloud_account_link" "foo" { + project_id = "%s" + cloud_account_id = "%s" +} +`, projectID, cloudAccountID) +} From 9f15b5c291fffb6e62d7ea7b7cd11894353122a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Mon, 29 Jul 2024 09:27:36 +0200 Subject: [PATCH 07/12] fix go-lint errors --- internal/acceptance/common.go | 2 +- internal/provider/resource_project_cloud_account_link.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/acceptance/common.go b/internal/acceptance/common.go index 859a606..102d4e3 100644 --- a/internal/acceptance/common.go +++ b/internal/acceptance/common.go @@ -25,6 +25,6 @@ const ( TcReportGraphQuery TestCase = "REPORT_GRAPH_QUERY" // TcCloudConfigRule test case TcCloudConfigRule TestCase = "CLOUD_CONFIG_RULE" - + // TcProjectCloudAccountLink test case TcProjectCloudAccountLink = "PROJECT_CLOUD_ACCOUNT_LINK" ) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index faa5665..6930e0e 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -25,7 +25,7 @@ type CloudAccountSearchResponse struct { GraphSearch struct { Nodes []struct { Entities []struct { - Id string `json:"id"` + ID string `json:"id"` } `json:"entities"` } `json:"nodes"` } `json:"graphSearch"` @@ -283,7 +283,7 @@ func searchForCloudAccount(ctx context.Context, externalID string, m interface{} return "", diag.Errorf("cloud account %s not found in wiz inventory", externalID) } - return respData.GraphSearch.Nodes[0].Entities[0].Id, nil + return respData.GraphSearch.Nodes[0].Entities[0].ID, nil } func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.ResourceData, m interface{}) (diags diag.Diagnostics) { @@ -294,9 +294,9 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res if v, ok := d.GetOk("cloud_account_id"); ok { cloudAccountWizID = v.(string) } else { - cloudAccountUpstreamId := d.Get("external_cloud_account_id").(string) + cloudAccountUpstreamID := d.Get("external_cloud_account_id").(string) var diagsSearch diag.Diagnostics - cloudAccountWizID, diagsSearch = searchForCloudAccount(ctx, cloudAccountUpstreamId, m) + cloudAccountWizID, diagsSearch = searchForCloudAccount(ctx, cloudAccountUpstreamID, m) if len(diagsSearch) > 0 { return diagsSearch } From e5273e27f80d2a7405983b24fe6fe86ed99f61c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Tue, 30 Jul 2024 09:06:03 +0200 Subject: [PATCH 08/12] format imports --- internal/provider/resource_project_cloud_account_link.go | 3 ++- .../provider/resource_project_cloud_account_link_test.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index 6930e0e..f62f5de 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -4,10 +4,11 @@ import ( "context" "errors" "fmt" - "github.com/google/uuid" "slices" "strings" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" diff --git a/internal/provider/resource_project_cloud_account_link_test.go b/internal/provider/resource_project_cloud_account_link_test.go index c789219..40f7f33 100644 --- a/internal/provider/resource_project_cloud_account_link_test.go +++ b/internal/provider/resource_project_cloud_account_link_test.go @@ -1,10 +1,11 @@ package provider import ( - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "reflect" "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "wiz.io/hashicorp/terraform-provider-wiz/internal/utils" "wiz.io/hashicorp/terraform-provider-wiz/internal/wiz" ) From 362abe0cf4744acdc648359fe084c5295132fb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Thu, 1 Aug 2024 13:15:16 +0200 Subject: [PATCH 09/12] trim graph query + update description of resource --- .../resource_project_cloud_account_link.go | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index f62f5de..031a5e6 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -67,8 +67,7 @@ type PatchProjectCloudAccountLinks struct { func resourceWizProjectCloudAccountLink() *schema.Resource { return &schema.Resource{ - Description: "Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. " + - "Link of a Project to a Cloud Account.", + Description: "Associate a cloud subscription with a project. Use either this resource or the cloud_account_link block set for the wiz_project, never both.", Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, @@ -375,53 +374,6 @@ func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.Resou id: $id ) { id - name - isFolder - ancestorProjects { - id - } - description - identifiers - slug - archived - businessUnit - projectOwners { - id - name - email - } - securityChampions { - id - name - email - } - riskProfile { - businessImpact - isActivelyDeveloped - hasAuthentication - hasExposedAPI - isInternetFacing - isCustomerFacing - storesData - sensitiveDataTypes - isRegulated - regulatoryStandards - } - cloudOrganizationLinks { - cloudOrganization { - externalId - id - name - path - } - resourceTags { - key - value - } - resourceGroups - shared - environment - } cloudAccountLinks { cloudAccount { externalId @@ -436,14 +388,6 @@ func resourceWizProjectCloudAccountLinkRead(ctx context.Context, d *schema.Resou shared environment } - kubernetesClustersLinks { - kubernetesCluster { - id - } - environment - namespaces - shared - } } }` From fb8dc431dd1cf2b2dc7504a978fe544be32cf878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Mon, 5 Aug 2024 10:35:48 +0200 Subject: [PATCH 10/12] resolve PR comments --- .../resource_project_cloud_account_link.go | 33 ++++--------------- internal/wiz/structs.go | 15 +++++++++ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/internal/provider/resource_project_cloud_account_link.go b/internal/provider/resource_project_cloud_account_link.go index 031a5e6..2eda444 100644 --- a/internal/provider/resource_project_cloud_account_link.go +++ b/internal/provider/resource_project_cloud_account_link.go @@ -23,13 +23,7 @@ import ( // CloudAccountSearchResponse represents the response from a cloud account search. // It includes a GraphSearch object that contains a list of Nodes, each with a list of Entities. type CloudAccountSearchResponse struct { - GraphSearch struct { - Nodes []struct { - Entities []struct { - ID string `json:"id"` - } `json:"entities"` - } `json:"nodes"` - } `json:"graphSearch"` + GraphSearch wiz.GraphSearchResultConnection `json:"graphSearch"` } // SearchForCloudAccountVars represents the variables for a cloud account search. @@ -52,19 +46,6 @@ type PartialProject struct { CloudAccountLinks []*wiz.ProjectCloudAccountLink } -// UpdateProjectCloudAccountLinks represents the input for updating project cloud account links. -// It includes the ID and a Patch object. -type UpdateProjectCloudAccountLinks struct { - ID string `json:"id"` - Patch PatchProjectCloudAccountLinks `json:"patch"` -} - -// PatchProjectCloudAccountLinks represents the patch object for updating project cloud account links. -// It includes a list of wiz.ProjectCloudAccountLinkInput. -type PatchProjectCloudAccountLinks struct { - CloudAccountLinks []*wiz.ProjectCloudAccountLinkInput `json:"cloudAccountLinks"` -} - func resourceWizProjectCloudAccountLink() *schema.Resource { return &schema.Resource{ Description: "Associate a cloud subscription with a project. Use either this resource or the cloud_account_link block set for the wiz_project, never both.", @@ -334,9 +315,9 @@ func resourceWizProjectCloudAccountLinkCreate(ctx context.Context, d *schema.Res }` // populate the graphql variables - vars := &UpdateProjectCloudAccountLinks{ + vars := &wiz.UpdateProjectCloudAccountLinks{ ID: projectID, - Patch: PatchProjectCloudAccountLinks{ + Patch: wiz.PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, } @@ -501,9 +482,9 @@ func resourceWizProjectCloudAccountLinkUpdate(ctx context.Context, d *schema.Res }` // populate the graphql variables - vars := &UpdateProjectCloudAccountLinks{ + vars := &wiz.UpdateProjectCloudAccountLinks{ ID: projectID, - Patch: PatchProjectCloudAccountLinks{ + Patch: wiz.PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, } @@ -552,9 +533,9 @@ func resourceWizProjectCloudAccountLinkDelete(ctx context.Context, d *schema.Res }` // populate the graphql variables - vars := &UpdateProjectCloudAccountLinks{ + vars := &wiz.UpdateProjectCloudAccountLinks{ ID: projectID, - Patch: PatchProjectCloudAccountLinks{ + Patch: wiz.PatchProjectCloudAccountLinks{ CloudAccountLinks: newCloudAccountLinksList, }, } diff --git a/internal/wiz/structs.go b/internal/wiz/structs.go index eee05d0..f0362f4 100644 --- a/internal/wiz/structs.go +++ b/internal/wiz/structs.go @@ -170,6 +170,21 @@ type UpdateProjectPatch struct { Slug string `json:"slug"` } +// UpdateProjectCloudAccountLinks represents the input for updating project cloud account links. +// It includes the ID and a Patch object. The type was initially considered to be UpdateProjectInput, +// but due to potential breaking changes with the addition of 'omitempty' to certain fields, this separate struct is used. +type UpdateProjectCloudAccountLinks struct { + ID string `json:"id"` + Patch PatchProjectCloudAccountLinks `json:"patch"` +} + +// PatchProjectCloudAccountLinks represents the patch object for updating project cloud account links. +// It includes a list of wiz.ProjectCloudAccountLinkInput. This struct is used instead of directly using UpdateProjectInput +// to avoid potential breaking changes with the addition of 'omitempty' to certain fields. +type PatchProjectCloudAccountLinks struct { + CloudAccountLinks []*ProjectCloudAccountLinkInput `json:"cloudAccountLinks"` +} + // UpdateSAMLIdentityProviderInput struct type UpdateSAMLIdentityProviderInput struct { ID string `json:"id"` From 34847ef4588759a24b175c2c39996473503f855b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Mon, 5 Aug 2024 10:43:24 +0200 Subject: [PATCH 11/12] run go generate --- docs/resources/project_cloud_account_link.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/project_cloud_account_link.md b/docs/resources/project_cloud_account_link.md index e1a1e68..14e29f3 100644 --- a/docs/resources/project_cloud_account_link.md +++ b/docs/resources/project_cloud_account_link.md @@ -3,12 +3,12 @@ page_title: "wiz_project_cloud_account_link Resource - terraform-provider-wiz" subcategory: "" description: |- - Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. Link of a Project to a Cloud Account. + Associate a cloud subscription with a project. Use either this resource or the cloudaccountlink block set for the wiz_project, never both. --- # wiz_project_cloud_account_link (Resource) -Please either use this resource or the embedded set of Cloud Account Links in the wiz_project resource. Link of a Project to a Cloud Account. +Associate a cloud subscription with a project. Use either this resource or the cloud_account_link block set for the wiz_project, never both. ## Example Usage From a40fb9034aab0a382d4bce63329160ef164f9b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B6bel=2C=20Jeremia?= Date: Tue, 6 Aug 2024 09:59:57 +0200 Subject: [PATCH 12/12] rename acceptance test --- ...t_link_test.go => resource_project_cloud_account_link_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/acceptance/{resource_wiz_project_cloud_account_link_test.go => resource_project_cloud_account_link_test.go} (100%) diff --git a/internal/acceptance/resource_wiz_project_cloud_account_link_test.go b/internal/acceptance/resource_project_cloud_account_link_test.go similarity index 100% rename from internal/acceptance/resource_wiz_project_cloud_account_link_test.go rename to internal/acceptance/resource_project_cloud_account_link_test.go