From c2389be47ba612cd90382b5b002e5efd0689ed68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 22 Mar 2024 17:35:10 +0000 Subject: [PATCH] Onboard Argus ACL (#304) * resource create and schema/model * consider empty value in resource creation * Address issue in mapfields that came up in testing * Unit testing the mapFields func * extend update * extend read * extend datasource.go * update example * extended acceptance tests and generated docs * update description and comments * improve messages and var names, fix update acceptance test * extend acceptance tests, improve error messages --- docs/data-sources/argus_instance.md | 1 + docs/resources/argus_instance.md | 2 + .../stackit_argus_instance/resource.tf | 1 + .../internal/services/argus/argus_acc_test.go | 171 +++++++++++++++++- .../services/argus/instance/datasource.go | 13 +- .../services/argus/instance/resource.go | 153 +++++++++++++++- .../services/argus/instance/resource_test.go | 60 +++++- 7 files changed, 379 insertions(+), 22 deletions(-) diff --git a/docs/data-sources/argus_instance.md b/docs/data-sources/argus_instance.md index 934102f0..ebf5652e 100644 --- a/docs/data-sources/argus_instance.md +++ b/docs/data-sources/argus_instance.md @@ -29,6 +29,7 @@ data "stackit_argus_instance" "example" { ### Read-Only +- `acl` (Set of String) The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation. - `alerting_url` (String) Specifies Alerting URL. - `dashboard_url` (String) Specifies Argus instance dashboard URL. - `grafana_initial_admin_password` (String, Sensitive) Specifies an initial Grafana admin password. diff --git a/docs/resources/argus_instance.md b/docs/resources/argus_instance.md index 75105306..e8189af9 100644 --- a/docs/resources/argus_instance.md +++ b/docs/resources/argus_instance.md @@ -17,6 +17,7 @@ resource "stackit_argus_instance" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-instance" plan_name = "Monitoring-Medium-EU01" + acl = ["1.1.1.1/32", "2.2.2.2/32"] } ``` @@ -31,6 +32,7 @@ resource "stackit_argus_instance" "example" { ### Optional +- `acl` (Set of String) The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation. - `parameters` (Map of String) Additional parameters. ### Read-Only diff --git a/examples/resources/stackit_argus_instance/resource.tf b/examples/resources/stackit_argus_instance/resource.tf index 6ebd0449..59b8b06c 100644 --- a/examples/resources/stackit_argus_instance/resource.tf +++ b/examples/resources/stackit_argus_instance/resource.tf @@ -2,4 +2,5 @@ resource "stackit_argus_instance" "example" { project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" name = "example-instance" plan_name = "Monitoring-Medium-EU01" + acl = ["1.1.1.1/32", "2.2.2.2/32"] } diff --git a/stackit/internal/services/argus/argus_acc_test.go b/stackit/internal/services/argus/argus_acc_test.go index df6bb997..467b1cd4 100644 --- a/stackit/internal/services/argus/argus_acc_test.go +++ b/stackit/internal/services/argus/argus_acc_test.go @@ -22,6 +22,9 @@ var instanceResource = map[string]string{ "name": testutil.ResourceNameWithDateTime("argus"), "plan_name": "Monitoring-Basic-EU01", "new_plan_name": "Monitoring-Medium-EU01", + "acl-0": "1.2.3.4/32", + "acl-1": "111.222.111.222/32", + "acl-1-updated": "111.222.111.125/32", } var scrapeConfigResource = map[string]string{ @@ -39,8 +42,9 @@ var credentialResource = map[string]string{ "project_id": testutil.ProjectId, } -func resourceConfig(instanceName, planName, target, saml2EnableUrlParameters string) string { - return fmt.Sprintf(` +func resourceConfig(acl *string, instanceName, planName, target, saml2EnableUrlParameters string) string { + if acl == nil { + return fmt.Sprintf(` %s resource "stackit_argus_instance" "instance" { @@ -68,10 +72,52 @@ func resourceConfig(instanceName, planName, target, saml2EnableUrlParameters str } `, + testutil.ArgusProviderConfig(), + instanceResource["project_id"], + instanceName, + planName, + scrapeConfigResource["name"], + scrapeConfigResource["metrics_path"], + target, + scrapeConfigResource["scrape_interval"], + scrapeConfigResource["sample_limit"], + saml2EnableUrlParameters, + ) + } + return fmt.Sprintf(` + %s + + resource "stackit_argus_instance" "instance" { + project_id = "%s" + name = "%s" + plan_name = "%s" + acl = %s + } + + resource "stackit_argus_scrapeconfig" "scrapeconfig" { + project_id = stackit_argus_instance.instance.project_id + instance_id = stackit_argus_instance.instance.instance_id + name = "%s" + metrics_path = "%s" + targets = [%s] + scrape_interval = "%s" + sample_limit = %s + saml2 = { + enable_url_parameters = %s + } + } + + resource "stackit_argus_credential" "credential" { + project_id = stackit_argus_instance.instance.project_id + instance_id = stackit_argus_instance.instance.instance_id + } + + `, testutil.ArgusProviderConfig(), instanceResource["project_id"], instanceName, planName, + *acl, scrapeConfigResource["name"], scrapeConfigResource["metrics_path"], target, @@ -88,7 +134,18 @@ func TestAccResource(t *testing.T) { Steps: []resource.TestStep{ // Creation { - Config: resourceConfig(instanceResource["name"], instanceResource["plan_name"], scrapeConfigResource["urls"], scrapeConfigResource["saml2_enable_url_parameters"]), + Config: resourceConfig( + utils.Ptr(fmt.Sprintf( + "[%q, %q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1"], + instanceResource["acl-1"], + )), + instanceResource["name"], + instanceResource["plan_name"], + scrapeConfigResource["urls"], + scrapeConfigResource["saml2_enable_url_parameters"], + ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_argus_instance.instance", "project_id", instanceResource["project_id"]), @@ -115,6 +172,11 @@ func TestAccResource(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "otlp_traces_url"), resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "zipkin_spans_url"), + // ACL + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.#", "2"), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.0", instanceResource["acl-0"]), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.1", instanceResource["acl-1"]), + // scrape config data resource.TestCheckResourceAttrPair( "stackit_argus_instance.instance", "project_id", @@ -141,7 +203,73 @@ func TestAccResource(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_argus_credential.credential", "username"), resource.TestCheckResourceAttrSet("stackit_argus_credential.credential", "password"), ), - }, { + }, + // Creation without ACL + { + Config: resourceConfig( + nil, + instanceResource["name"], + instanceResource["plan_name"], + scrapeConfigResource["urls"], + scrapeConfigResource["saml2_enable_url_parameters"], + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Instance data + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "project_id", instanceResource["project_id"]), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "instance_id"), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "name", instanceResource["name"]), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "plan_name", instanceResource["plan_name"]), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "dashboard_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "is_updatable"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "grafana_public_read_access"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "grafana_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "grafana_initial_admin_user"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "grafana_initial_admin_password"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "metrics_retention_days"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "metrics_retention_days_5m_downsampling"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "metrics_retention_days_1h_downsampling"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "metrics_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "metrics_push_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "targets_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "alerting_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "logs_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "logs_push_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "jaeger_traces_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "jaeger_ui_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "otlp_traces_url"), + resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "zipkin_spans_url"), + + // ACL + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.#", "0"), + + // scrape config data + resource.TestCheckResourceAttrPair( + "stackit_argus_instance.instance", "project_id", + "stackit_argus_scrapeconfig.scrapeconfig", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_argus_instance.instance", "instance_id", + "stackit_argus_scrapeconfig.scrapeconfig", "instance_id", + ), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "name", scrapeConfigResource["name"]), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "targets.0.urls.#", "2"), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "metrics_path", scrapeConfigResource["metrics_path"]), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "scheme", scrapeConfigResource["scheme"]), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "scrape_interval", scrapeConfigResource["scrape_interval"]), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "sample_limit", scrapeConfigResource["sample_limit"]), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters", scrapeConfigResource["saml2_enable_url_parameters"]), + + // credentials + resource.TestCheckResourceAttr("stackit_argus_credential.credential", "project_id", credentialResource["project_id"]), + resource.TestCheckResourceAttrPair( + "stackit_argus_instance.instance", "instance_id", + "stackit_argus_credential.credential", "instance_id", + ), + resource.TestCheckResourceAttrSet("stackit_argus_credential.credential", "username"), + resource.TestCheckResourceAttrSet("stackit_argus_credential.credential", "password"), + ), + }, + { // Data source Config: fmt.Sprintf(` %s @@ -157,7 +285,16 @@ func TestAccResource(t *testing.T) { name = stackit_argus_scrapeconfig.scrapeconfig.name } `, - resourceConfig(instanceResource["name"], instanceResource["plan_name"], scrapeConfigResource["urls"], scrapeConfigResource["saml2_enable_url_parameters"]), + resourceConfig(utils.Ptr(fmt.Sprintf( + "[%q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1"], + )), + instanceResource["name"], + instanceResource["plan_name"], + scrapeConfigResource["urls"], + scrapeConfigResource["saml2_enable_url_parameters"], + ), ), Check: resource.ComposeAggregateTestCheckFunc( // Instance data @@ -165,6 +302,9 @@ func TestAccResource(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_argus_instance.instance", "instance_id"), resource.TestCheckResourceAttr("data.stackit_argus_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("data.stackit_argus_instance.instance", "plan_name", instanceResource["plan_name"]), + resource.TestCheckResourceAttr("data.stackit_argus_instance.instance", "acl.#", "2"), + resource.TestCheckResourceAttr("data.stackit_argus_instance.instance", "acl.0", instanceResource["acl-0"]), + resource.TestCheckResourceAttr("data.stackit_argus_instance.instance", "acl.1", instanceResource["acl-1"]), resource.TestCheckResourceAttrPair( "stackit_argus_instance.instance", "project_id", "data.stackit_argus_instance.instance", "project_id", @@ -236,13 +376,25 @@ func TestAccResource(t *testing.T) { }, // Update { - Config: resourceConfig(fmt.Sprintf("%s-new", instanceResource["name"]), instanceResource["new_plan_name"], "", "true"), + Config: resourceConfig(utils.Ptr(fmt.Sprintf( + "[%q, %q]", + instanceResource["acl-0"], + instanceResource["acl-1-updated"], + )), + fmt.Sprintf("%s-new", instanceResource["name"]), + instanceResource["new_plan_name"], + "", + "true", + ), Check: resource.ComposeAggregateTestCheckFunc( // Instance resource.TestCheckResourceAttr("stackit_argus_instance.instance", "project_id", instanceResource["project_id"]), resource.TestCheckResourceAttrSet("stackit_argus_instance.instance", "instance_id"), resource.TestCheckResourceAttr("stackit_argus_instance.instance", "name", instanceResource["name"]+"-new"), resource.TestCheckResourceAttr("stackit_argus_instance.instance", "plan_name", instanceResource["new_plan_name"]), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.#", "2"), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.0", instanceResource["acl-0"]), + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.1", instanceResource["acl-1-updated"]), // Scrape Config resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "name", scrapeConfigResource["name"]), @@ -300,6 +452,9 @@ func TestAccResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_argus_instance.instance", "name", instanceResource["name"]), resource.TestCheckResourceAttr("stackit_argus_instance.instance", "plan_name", instanceResource["new_plan_name"]), + // ACL + resource.TestCheckResourceAttr("stackit_argus_instance.instance", "acl.#", "0"), + // Scrape Config resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "name", scrapeConfigResource["name"]), resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "targets.#", "1"), @@ -307,8 +462,8 @@ func TestAccResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "scheme", scrapeConfigResource["scheme"]), resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "scrape_interval", scrapeConfigResource["scrape_interval"]), resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "sample_limit", scrapeConfigResource["sample_limit"]), - resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "saml2.%", "0"), - resource.TestCheckNoResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters"), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "saml2.%", "1"), + resource.TestCheckResourceAttr("stackit_argus_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters", "false"), ), }, diff --git a/stackit/internal/services/argus/instance/datasource.go b/stackit/internal/services/argus/instance/datasource.go index 03d3d5b5..bc38e5a0 100644 --- a/stackit/internal/services/argus/instance/datasource.go +++ b/stackit/internal/services/argus/instance/datasource.go @@ -196,6 +196,11 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques "zipkin_spans_url": schema.StringAttribute{ Computed: true, }, + "acl": schema.SetAttribute{ + Description: "The access control list for this instance. Each entry is a single IP address that is permitted to access, in CIDR notation (/32).", + ElementType: types.StringType, + Computed: true, + }, }, } } @@ -216,7 +221,13 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques return } - err = mapFields(ctx, instanceResponse, &model) + aclList, err := d.client.ListACL(ctx, instanceId, projectId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) + return + } + + err = mapFields(ctx, instanceResponse, aclList, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/argus/instance/resource.go b/stackit/internal/services/argus/instance/resource.go index b00f76f6..ed18aaf5 100644 --- a/stackit/internal/services/argus/instance/resource.go +++ b/stackit/internal/services/argus/instance/resource.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -17,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/argus" "github.com/stackitcloud/stackit-sdk-go/services/argus/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" @@ -58,6 +60,7 @@ type Model struct { JaegerUIURL types.String `tfsdk:"jaeger_ui_url"` OtlpTracesURL types.String `tfsdk:"otlp_traces_url"` ZipkinSpansURL types.String `tfsdk:"zipkin_spans_url"` + ACL types.Set `tfsdk:"acl"` } // NewInstanceResource is a helper function to simplify the provider implementation. @@ -250,6 +253,16 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "zipkin_spans_url": schema.StringAttribute{ Computed: true, }, + "acl": schema.SetAttribute{ + Description: "The access control list for this instance. Each entry is a single IP address that is permitted to access, in CIDR notation (/32).", + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + validate.CIDR(), + ), + }, + }, }, } } @@ -264,6 +277,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques return } + acl := []string{} + if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + diags = model.ACL.ElementsAs(ctx, &acl, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -273,12 +295,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques return } // Generate API request body from model - payload, err := toCreatePayload(&model) + createPayload, err := toCreatePayload(&model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) return } - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() + createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*createPayload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return @@ -292,11 +314,38 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } // Map response body to schema - err = mapFields(ctx, waitResp, &model) + err = mapFields(ctx, waitResp, nil, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + + // Set state to instance populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Create ACL + err = updateACL(ctx, projectId, *instanceId, acl, r.client) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACL: %v", err)) + return + } + aclList, err := r.client.ListACL(ctx, *instanceId, projectId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, waitResp, aclList, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response: %v", err)) + return + } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) @@ -325,19 +374,27 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } + aclList, err := r.client.ListACL(ctx, instanceId, projectId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACL data: %v", err)) + return + } + // Map response body to schema - err = mapFields(ctx, instanceResp, &model) + err = mapFields(ctx, instanceResp, aclList, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } - // Set refreshed model + + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "Argus instance created") + + tflog.Info(ctx, "Argus instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -352,6 +409,15 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() + acl := []string{} + if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + diags = model.ACL.ElementsAs(ctx, &acl, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + err := r.loadPlanId(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) @@ -376,7 +442,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques return } - err = mapFields(ctx, waitResp, &model) + err = mapFields(ctx, waitResp, nil, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -386,6 +452,33 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques if resp.Diagnostics.HasError() { return } + + // Update ACL + err = updateACL(ctx, projectId, instanceId, acl, r.client) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Updating ACL: %v", err)) + return + } + aclList, err := r.client.ListACL(ctx, instanceId, projectId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, waitResp, aclList, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing ACL API payload: %v", err)) + return + } + + // Set state to ACL populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus instance updated") } @@ -435,7 +528,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS tflog.Info(ctx, "Argus instance state imported") } -func mapFields(ctx context.Context, r *argus.GetInstanceResponse, model *Model) error { +func mapFields(ctx context.Context, r *argus.GetInstanceResponse, acl *argus.ListACLResponse, model *Model) error { if r == nil { return fmt.Errorf("response input is nil") } @@ -500,6 +593,37 @@ func mapFields(ctx context.Context, r *argus.GetInstanceResponse, model *Model) model.OtlpTracesURL = types.StringPointerValue(i.OtlpTracesUrl) model.ZipkinSpansURL = types.StringPointerValue(i.ZipkinSpansUrl) } + + err := mapACLField(acl, model) + if err != nil { + return err + } + + return nil +} + +func mapACLField(aclList *argus.ListACLResponse, model *Model) error { + if aclList == nil { + if model.ACL.IsNull() || model.ACL.IsUnknown() { + model.ACL = types.SetNull(types.StringType) + } + return nil + } + + if aclList.Acl == nil || len(*aclList.Acl) == 0 { + model.ACL = types.SetNull(types.StringType) + return nil + } + + acl := []attr.Value{} + for _, cidr := range *aclList.Acl { + acl = append(acl, types.StringValue(cidr)) + } + aclTF, diags := types.SetValue(types.StringType, acl) + if diags.HasError() { + return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) + } + model.ACL = aclTF return nil } @@ -519,6 +643,19 @@ func toCreatePayload(model *Model) (*argus.CreateInstancePayload, error) { }, nil } +func updateACL(ctx context.Context, projectId, instanceId string, acl []string, client *argus.APIClient) error { + payload := argus.UpdateACLPayload{ + Acl: utils.Ptr(acl), + } + + _, err := client.UpdateACL(ctx, instanceId, projectId).UpdateACLPayload(payload).Execute() + if err != nil { + return fmt.Errorf("updating ACL: %w", err) + } + + return nil +} + func toUpdatePayload(model *Model) (*argus.UpdateInstancePayload, error) { if model == nil { return nil, fmt.Errorf("nil model") diff --git a/stackit/internal/services/argus/instance/resource_test.go b/stackit/internal/services/argus/instance/resource_test.go index 3136ec10..fcce8bfe 100644 --- a/stackit/internal/services/argus/instance/resource_test.go +++ b/stackit/internal/services/argus/instance/resource_test.go @@ -14,16 +14,18 @@ import ( func TestMapFields(t *testing.T) { tests := []struct { - description string - input *argus.GetInstanceResponse - expected Model - isValid bool + description string + instanceResp *argus.GetInstanceResponse + listACLResp *argus.ListACLResponse + expected Model + isValid bool }{ { "default_ok", &argus.GetInstanceResponse{ Id: utils.Ptr("iid"), }, + nil, Model{ Id: types.StringValue("pid,iid"), ProjectId: types.StringValue("pid"), @@ -32,6 +34,7 @@ func TestMapFields(t *testing.T) { PlanName: types.StringNull(), Name: types.StringNull(), Parameters: types.MapNull(types.StringType), + ACL: types.SetNull(types.StringType), }, true, }, @@ -44,6 +47,12 @@ func TestMapFields(t *testing.T) { PlanId: utils.Ptr("planId"), Parameters: &map[string]string{"key": "value"}, }, + &argus.ListACLResponse{ + Acl: &[]string{ + "1.1.1.1/32", + }, + Message: utils.Ptr("message"), + }, Model{ Id: types.StringValue("pid,iid"), ProjectId: types.StringValue("pid"), @@ -52,6 +61,40 @@ func TestMapFields(t *testing.T) { PlanId: types.StringValue("planId"), PlanName: types.StringValue("plan1"), Parameters: toTerraformStringMapMust(context.Background(), map[string]string{"key": "value"}), + ACL: types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("1.1.1.1/32"), + }), + }, + true, + }, + { + "values_ok_multiple_acls", + &argus.GetInstanceResponse{ + Id: utils.Ptr("iid"), + Name: utils.Ptr("name"), + PlanName: utils.Ptr("plan1"), + PlanId: utils.Ptr("planId"), + Parameters: &map[string]string{"key": "value"}, + }, + &argus.ListACLResponse{ + Acl: &[]string{ + "1.1.1.1/32", + "8.8.8.8/32", + }, + Message: utils.Ptr("message"), + }, + Model{ + Id: types.StringValue("pid,iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + InstanceId: types.StringValue("iid"), + PlanId: types.StringValue("planId"), + PlanName: types.StringValue("plan1"), + Parameters: toTerraformStringMapMust(context.Background(), map[string]string{"key": "value"}), + ACL: types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("1.1.1.1/32"), + types.StringValue("8.8.8.8/32"), + }), }, true, }, @@ -61,6 +104,10 @@ func TestMapFields(t *testing.T) { Id: utils.Ptr("iid"), Name: nil, }, + &argus.ListACLResponse{ + Acl: &[]string{}, + Message: nil, + }, Model{ Id: types.StringValue("pid,iid"), ProjectId: types.StringValue("pid"), @@ -69,18 +116,21 @@ func TestMapFields(t *testing.T) { PlanName: types.StringNull(), Name: types.StringNull(), Parameters: types.MapNull(types.StringType), + ACL: types.SetNull(types.StringType), }, true, }, { "response_nil_fail", nil, + nil, Model{}, false, }, { "no_resource_id", &argus.GetInstanceResponse{}, + nil, Model{}, false, }, @@ -90,7 +140,7 @@ func TestMapFields(t *testing.T) { state := &Model{ ProjectId: tt.expected.ProjectId, } - err := mapFields(context.Background(), tt.input, state) + err := mapFields(context.Background(), tt.instanceResp, tt.listACLResp, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") }