diff --git a/examples/data-sources/stackit_telemetrylink/data-source.tf b/examples/data-sources/stackit_telemetrylink/data-source.tf new file mode 100644 index 000000000..932becea4 --- /dev/null +++ b/examples/data-sources/stackit_telemetrylink/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_telemetrylink" "link" { + resource_type = "project" + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" +} \ No newline at end of file diff --git a/examples/resources/stackit_telemetrylink/resource.tf b/examples/resources/stackit_telemetrylink/resource.tf new file mode 100644 index 000000000..5d03433a2 --- /dev/null +++ b/examples/resources/stackit_telemetrylink/resource.tf @@ -0,0 +1,24 @@ +resource "stackit_telemetrylink" "link" { + resource_type = "project" + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + display_name = "telemetrylink-example" + access_token = "eyJxxx" + telemetry_router_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_telemetrylink" "link2" { + resource_type = "project" + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + region = "eu01" + display_name = "telemetrylink-example" + description = "telemetrylink description" + access_token = "eyJxxx" + telemetry_router_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +# Only use the import statement, if you want to import an existing TelemetryLink +import { + to = stackit_telemetrylink.import-example + id = "${var.resource_type},${var.resource_id},${var.region}" +} diff --git a/go.mod b/go.mod index 34a688097..01fa8e9b6 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/sfs v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 + github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.1.1 github.com/teambition/rrule-go v1.8.2 golang.org/x/mod v0.35.0 ) diff --git a/go.sum b/go.sum index 2afdeb2c8..b7ec348dd 100644 --- a/go.sum +++ b/go.sum @@ -734,6 +734,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0 h1:Zy3yxmHzW+ydu1nae github.com/stackitcloud/stackit-sdk-go/services/ske v1.14.0/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0 h1:angvO3z0TGqZtdwTDsG/tgTw9hxB76A6leUsiUXQtME= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.10.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M= +github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.1.1 h1:+YxN37hx3bj55c/CloXOoQTYLzwLp1Cf0NvkLemZECE= +github.com/stackitcloud/stackit-sdk-go/services/telemetrylink v0.1.1/go.mod h1:hgw8janWmDfP2bnuZensxqcAePr49BX5ug8Rq85o+h8= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 3ecf92de1..ad5a13c48 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -72,6 +72,7 @@ type ProviderData struct { ServiceEnablementCustomEndpoint string SfsCustomEndpoint string ServiceAccountCustomEndpoint string + TelemetryLinkCustomEndpoint string EnableBetaResources bool Experiments []string diff --git a/stackit/internal/services/telemetrylink/link/datasource.go b/stackit/internal/services/telemetrylink/link/datasource.go new file mode 100644 index 000000000..39d98de13 --- /dev/null +++ b/stackit/internal/services/telemetrylink/link/datasource.go @@ -0,0 +1,212 @@ +package link + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/utils" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &telemetryLinkLinkDataSource{} +) + +func NewTelemetryLinkLinkDataSource() datasource.DataSource { + return &telemetryLinkLinkDataSource{} +} + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` // Required by Terraform + LinkID types.String `tfsdk:"link_id"` + Region types.String `tfsdk:"region"` + ResourceType types.String `tfsdk:"resource_type"` + ResourceID types.String `tfsdk:"resource_id"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + TelemetryRouterID types.String `tfsdk:"telemetry_router_id"` + CreateTime types.String `tfsdk:"create_time"` + Status types.String `tfsdk:"status"` +} + +type telemetryLinkLinkDataSource struct { + client *telemetrylink.APIClient + providerData core.ProviderData +} + +func (d *telemetryLinkLinkDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_telemetrylink" +} + +func (d *telemetryLinkLinkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + d.providerData = providerData + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "TelemetryLink client configured") +} + +func (d *telemetryLinkLinkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("TelemetryLink data source schema. %s", core.DatasourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: schemaDescriptions["id"], + Computed: true, + }, + "link_id": schema.StringAttribute{ + Description: schemaDescriptions["instance_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "resource_type": schema.StringAttribute{ + Description: schemaDescriptions["resource_type"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(resourceTypes...), + validate.NoSeparator(), + }, + }, + "resource_id": schema.StringAttribute{ + Description: schemaDescriptions["resource_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: schemaDescriptions["region"], + // the region cannot be found, so it has to be passed + Optional: true, + }, + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["display_name"], + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "description": schema.StringAttribute{ + Description: schemaDescriptions["description"], + Computed: true, + }, + "telemetry_router_id": schema.StringAttribute{ + Description: schemaDescriptions["telemetry_router_id"], + Computed: true, + }, + "create_time": schema.StringAttribute{ + Description: schemaDescriptions["create_time"], + Computed: true, + }, + "status": schema.StringAttribute{ + Description: schemaDescriptions["status"], + Computed: true, + }, + }, + } +} + +func (d *telemetryLinkLinkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + resourceType := model.ResourceType.ValueString() + resourceID := model.ResourceID.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "resource_type", resourceType) + ctx = tflog.SetField(ctx, "resource_id", resourceID) + ctx = tflog.SetField(ctx, "region", region) + + var response *telemetrylink.TelemetryLinkResponse + var err error + switch resourceType { + case resourceTypeOrganization: + response, err = d.client.DefaultAPI.GetOrganizationTelemetryLink(ctx, resourceID, region).Execute() + case resourceTypeFolder: + response, err = d.client.DefaultAPI.GetFolderTelemetryLink(ctx, resourceID, region).Execute() + case resourceTypeProject: + response, err = d.client.DefaultAPI.GetProjectTelemetryLink(ctx, resourceID, region).Execute() + } + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = core.LogResponse(ctx) + + err = mapDataSourceFields(ctx, response, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading TelemetryLink", fmt.Sprintf("Processing response: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "TelemetryLink read", map[string]interface{}{ + "resource_type": resourceType, + "resource_id": resourceID, + }) +} + +func mapDataSourceFields(_ context.Context, link *telemetrylink.TelemetryLinkResponse, model *DataSourceModel) error { + if link == nil { + return fmt.Errorf("link is nil") + } + if model == nil { + return fmt.Errorf("model is nil") + } + var linkID string + if model.LinkID.ValueString() != "" { + linkID = model.LinkID.ValueString() + } else { + linkID = link.Id + } + + model.ID = tfutils.BuildInternalTerraformId(model.ResourceType.ValueString(), model.ResourceID.ValueString(), model.Region.ValueString()) + model.LinkID = types.StringValue(linkID) + model.DisplayName = types.StringValue(link.DisplayName) + model.Description = types.StringPointerValue(link.Description) + model.TelemetryRouterID = types.StringValue(link.TelemetryRouterId) + model.CreateTime = types.StringValue(link.CreateTime.String()) + model.Status = types.StringValue(link.Status) + + return nil +} diff --git a/stackit/internal/services/telemetrylink/link/datasource_test.go b/stackit/internal/services/telemetrylink/link/datasource_test.go new file mode 100644 index 000000000..093cb116f --- /dev/null +++ b/stackit/internal/services/telemetrylink/link/datasource_test.go @@ -0,0 +1,85 @@ +package link + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" +) + +func fixtureDataSourceModel(mods ...func(model *DataSourceModel)) *DataSourceModel { + model := &DataSourceModel{ + ID: types.StringValue("rtp,rid,reg"), + LinkID: types.StringValue("lid"), + Region: types.StringValue("reg"), + ResourceType: types.StringValue("rtp"), + ResourceID: types.StringValue("rid"), + DisplayName: types.StringValue("name"), + Description: types.String{}, + TelemetryRouterID: types.StringValue("tlmrid"), + CreateTime: types.StringValue(testTime.String()), + Status: types.StringValue("active"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + input *telemetrylink.TelemetryLinkResponse + expected *DataSourceModel + wantErr bool + }{ + { + description: "min values", + input: fixtureLink(), + expected: fixtureDataSourceModel(), + }, + { + description: "max values", + input: fixtureLink(func(link *telemetrylink.TelemetryLinkResponse) { + link.Description = new("description") + link.DisplayName = "display-name" + link.AccessToken = new("access-token") + link.TelemetryRouterId = "tlmr-id" + }), + expected: fixtureDataSourceModel(func(model *DataSourceModel) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.TelemetryRouterID = types.StringValue("tlmr-id") + }), + }, + { + description: "nil input", + wantErr: true, + expected: fixtureDataSourceModel(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &DataSourceModel{ + ResourceType: tt.expected.ResourceType, + ResourceID: tt.expected.ResourceID, + Region: tt.expected.Region, + } + err := mapDataSourceFields(context.Background(), tt.input, state) + if tt.wantErr && err == nil { + t.Fatalf("Should have failed") + } + if !tt.wantErr && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if !tt.wantErr { + diff := cmp.Diff(state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/telemetrylink/link/resource.go b/stackit/internal/services/telemetrylink/link/resource.go new file mode 100644 index 000000000..fb596ddee --- /dev/null +++ b/stackit/internal/services/telemetrylink/link/resource.go @@ -0,0 +1,657 @@ +package link + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi/wait" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/utils" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +const ( + resourceTypeOrganization = "organization" + resourceTypeFolder = "folder" + resourceTypeProject = "project" +) + +var ( + _ resource.Resource = &telemetryLinkInstanceResource{} + _ resource.ResourceWithConfigure = &telemetryLinkInstanceResource{} + _ resource.ResourceWithImportState = &telemetryLinkInstanceResource{} + _ resource.ResourceWithModifyPlan = &telemetryLinkInstanceResource{} + + resourceTypes = []string{resourceTypeOrganization, resourceTypeFolder, resourceTypeProject} +) + +var schemaDescriptions = map[string]string{ + "id": "Terraform's internal resource identifier. It is structured as \"`resource_type`, `resource_id`,`region`\".", + "link_id": "The TelemetryLink ID", + "region": "STACKIT region name the resource is located in. If not defined, the provider region is used.", + "resource_type": fmt.Sprintf( + "The resource type of the TelemetryLink resource, possible values: %s", + tfutils.FormatPossibleValues(resourceTypes...), + ), + "resource_id": "STACKIT project ID, folder ID, or organization ID associated with the Telemetry Link resource.", + "display_name": "The displayed name of the Telemetry Link resource.", + "description": "The description of the Telemetry Link resource.", + "telemetry_router_id": "The Telemetry Router ID.", + "access_token": "The access token of the Telemetry Router instance.", + "create_time": "The time the Telemetry Link was created.", + "status": fmt.Sprintf( + "The status of the TelemetryLink, possible values: %s", + tfutils.FormatPossibleValues("active", "inactive", "failed"), + ), +} + +type Model struct { + ID types.String `tfsdk:"id"` // Required by Terraform + LinkID types.String `tfsdk:"link_id"` + Region types.String `tfsdk:"region"` + ResourceType types.String `tfsdk:"resource_type"` + ResourceID types.String `tfsdk:"resource_id"` + DisplayName types.String `tfsdk:"display_name"` + Description types.String `tfsdk:"description"` + TelemetryRouterID types.String `tfsdk:"telemetry_router_id"` + AccessToken types.String `tfsdk:"access_token"` + CreateTime types.String `tfsdk:"create_time"` + Status types.String `tfsdk:"status"` +} + +type telemetryLinkInstanceResource struct { + client *telemetrylink.APIClient + providerData core.ProviderData +} + +func NewTelemetryLinkResource() resource.Resource { + return &telemetryLinkInstanceResource{} +} + +func (r *telemetryLinkInstanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := utils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + r.providerData = providerData + tflog.Info(ctx, "TelemetryLink client configured") +} + +func (r *telemetryLinkInstanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + tfutils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *telemetryLinkInstanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_telemetrylink" +} + +func (r *telemetryLinkInstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: fmt.Sprintf("TelemetryLink instance resource schema. %s", core.ResourceRegionFallbackDocstring), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: schemaDescriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "link_id": schema.StringAttribute{ + Description: schemaDescriptions["link_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "resource_type": schema.StringAttribute{ + Description: schemaDescriptions["resource_type"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(resourceTypes...), + validate.NoSeparator(), + }, + }, + "resource_id": schema.StringAttribute{ + Description: schemaDescriptions["resource_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "display_name": schema.StringAttribute{ + Description: schemaDescriptions["display_name"], + Required: true, + }, + "description": schema.StringAttribute{ + Description: schemaDescriptions["description"], + Optional: true, + }, + "telemetry_router_id": schema.StringAttribute{ + Description: schemaDescriptions["telemetry_router_id"], + Required: true, + }, + "region": schema.StringAttribute{ + Description: schemaDescriptions["region"], + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "access_token": schema.StringAttribute{ + Description: schemaDescriptions["access_token"], + Optional: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "create_time": schema.StringAttribute{ + Description: schemaDescriptions["create_time"], + Computed: true, + }, + "status": schema.StringAttribute{ + Description: schemaDescriptions["status"], + Computed: true, + }, + }, + } +} + +func (r *telemetryLinkInstanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + resourceType := model.ResourceType.ValueString() + resourceID := model.ResourceID.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "resource_type", resourceType) + ctx = tflog.SetField(ctx, "resource_id", resourceID) + ctx = tflog.SetField(ctx, "region", region) + + regionId := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "region", regionId) + + var response *telemetrylink.TelemetryLinkResponse + switch model.ResourceType.ValueString() { + case resourceTypeOrganization: + payload, err := toCreateOrUpdateOrganizationTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.CreateOrUpdateOrganizationTelemetryLink(ctx, resourceID, regionId).CreateOrUpdateOrganizationTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.CreateOrUpdateOrganizationTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + + case resourceTypeFolder: + payload, err := toCreateOrUpdateFolderTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.CreateOrUpdateFolderTelemetryLink(ctx, resourceID, regionId).CreateOrUpdateFolderTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.CreateOrUpdateFolderTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + case resourceTypeProject: + payload, err := toCreateOrUpdateProjectTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.CreateOrUpdateProjectTelemetryLink(ctx, resourceID, regionId).CreateOrUpdateProjectTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.CreateOrUpdateProjectTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + } + + err := mapFields(ctx, response, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating TelemetryLink", fmt.Sprintf("Processing response: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "TelemetryLink created") +} + +func (r *telemetryLinkInstanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + resourceType := model.ResourceType.ValueString() + resourceID := model.ResourceID.ValueString() + region := model.Region.ValueString() + + ctx = tflog.SetField(ctx, "resource_type", resourceType) + ctx = tflog.SetField(ctx, "resource_id", resourceID) + ctx = tflog.SetField(ctx, "region", region) + + var err error + var response *telemetrylink.TelemetryLinkResponse + switch resourceType { + case resourceTypeOrganization: + response, err = r.client.DefaultAPI.GetOrganizationTelemetryLink(ctx, resourceID, region).Execute() + case resourceTypeFolder: + response, err = r.client.DefaultAPI.GetFolderTelemetryLink(ctx, resourceID, region).Execute() + case resourceTypeProject: + response, err = r.client.DefaultAPI.GetProjectTelemetryLink(ctx, resourceID, region).Execute() + } + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, response, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading TelemetryLink", fmt.Sprintf("Processing response: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "TelemetryLink read", map[string]interface{}{ + "resource_type": resourceType, + "resource_id": resourceID, + }) +} + +func (r *telemetryLinkInstanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + resourceType := model.ResourceType.ValueString() + resourceID := model.ResourceID.ValueString() + region := model.Region.ValueString() + + ctx = tflog.SetField(ctx, "resource_type", resourceType) + ctx = tflog.SetField(ctx, "resource_id", resourceID) + ctx = tflog.SetField(ctx, "region", region) + + regionId := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "region", regionId) + + var response *telemetrylink.TelemetryLinkResponse + switch model.ResourceType.ValueString() { + case resourceTypeOrganization: + payload, err := toPartialUpdateOrganizationTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.PartialUpdateOrganizationTelemetryLink(ctx, resourceID, regionId).PartialUpdateOrganizationTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.PartialUpdateOrganizationTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + case resourceTypeFolder: + payload, err := toPartialUpdateFolderTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.PartialUpdateFolderTelemetryLink(ctx, resourceID, regionId).PartialUpdateFolderTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.PartialUpdateFolderTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + case resourceTypeProject: + payload, err := toPartialUpdateProjectTelemetryLinkPayload(ctx, resp.Diagnostics, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + _, err = r.client.DefaultAPI.PartialUpdateProjectTelemetryLink(ctx, resourceID, regionId).PartialUpdateProjectTelemetryLinkPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + response, err = wait.PartialUpdateProjectTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, regionId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become active: %v", err)) + return + } + } + + err := mapFields(ctx, response, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating TelemetryLink", fmt.Sprintf("Processing response: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "TelemetryLink updated", map[string]interface{}{ + "resource_type": resourceType, + "resource_id": resourceID, + }) +} + +func (r *telemetryLinkInstanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + resourceType := model.ResourceType.ValueString() + resourceID := model.ResourceID.ValueString() + region := model.Region.ValueString() + + ctx = tflog.SetField(ctx, "resource_type", resourceType) + ctx = tflog.SetField(ctx, "resource_id", resourceID) + ctx = tflog.SetField(ctx, "region", region) + + var err error + switch resourceType { + case resourceTypeOrganization: + err = r.client.DefaultAPI.DeleteOrganizationTelemetryLink(ctx, resourceID, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.DeleteOrganizationTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become deleted: %v", err)) + return + } + case resourceTypeFolder: + err = r.client.DefaultAPI.DeleteFolderTelemetryLink(ctx, resourceID, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.DeleteFolderTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become deleted: %v", err)) + return + } + case resourceTypeProject: + err = r.client.DefaultAPI.DeleteProjectTelemetryLink(ctx, resourceID, region).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + _, err = wait.DeleteProjectTelemetryLinkWaitHandler(ctx, r.client.DefaultAPI, resourceID, region).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting TelemetryLink", fmt.Sprintf("Waiting for TelemetryLink to become deleted: %v", err)) + return + } + } + + tflog.Info(ctx, "TelemetryLink deleted") +} + +func (r *telemetryLinkInstanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing TelemetryLink", fmt.Sprintf("Invalid import ID %q: expected format is `project_id`,`region`,`instance_id`", req.ID)) + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("resource_type"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("resource_id"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[2])...) + tflog.Info(ctx, "TelemetryLink state imported") +} + +func toPartialUpdateOrganizationTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueStringPointer(), + AccessToken: model.AccessToken.ValueStringPointer(), + }, nil +} + +func toPartialUpdateFolderTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.PartialUpdateFolderTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueStringPointer(), + AccessToken: model.AccessToken.ValueStringPointer(), + }, nil +} + +func toPartialUpdateProjectTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.PartialUpdateProjectTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueStringPointer(), + AccessToken: model.AccessToken.ValueStringPointer(), + }, nil +} + +func toCreateOrUpdateOrganizationTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueString(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueString(), + AccessToken: model.AccessToken.ValueString(), + }, nil +} + +func toCreateOrUpdateFolderTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueString(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueString(), + AccessToken: model.AccessToken.ValueString(), + }, nil +} + +func toCreateOrUpdateProjectTelemetryLinkPayload(_ context.Context, _ diag.Diagnostics, model *Model) (*telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload, error) { + if model == nil { + return nil, fmt.Errorf("missing model") + } + + return &telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + DisplayName: model.DisplayName.ValueString(), + Description: model.Description.ValueStringPointer(), + TelemetryRouterId: model.TelemetryRouterID.ValueString(), + AccessToken: model.AccessToken.ValueString(), + }, nil +} + +func mapFields(_ context.Context, link *telemetrylink.TelemetryLinkResponse, model *Model) error { + if link == nil { + return fmt.Errorf("link is nil") + } + if model == nil { + return fmt.Errorf("model is nil") + } + var linkID string + if model.LinkID.ValueString() != "" { + linkID = model.LinkID.ValueString() + } else { + linkID = link.Id + } + + model.ID = tfutils.BuildInternalTerraformId(model.ResourceType.ValueString(), model.ResourceID.ValueString(), model.Region.ValueString()) + model.LinkID = types.StringValue(linkID) + model.DisplayName = types.StringValue(link.DisplayName) + model.Description = types.StringPointerValue(link.Description) + model.TelemetryRouterID = types.StringValue(link.TelemetryRouterId) + model.CreateTime = types.StringValue(link.CreateTime.String()) + model.Status = types.StringValue(link.Status) + + return nil +} diff --git a/stackit/internal/services/telemetrylink/link/resource_test.go b/stackit/internal/services/telemetrylink/link/resource_test.go new file mode 100644 index 000000000..446bd2faf --- /dev/null +++ b/stackit/internal/services/telemetrylink/link/resource_test.go @@ -0,0 +1,422 @@ +package link + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" +) + +var testTime = time.Now() + +func fixtureLink(mods ...func(link *telemetrylink.TelemetryLinkResponse)) *telemetrylink.TelemetryLinkResponse { + link := &telemetrylink.TelemetryLinkResponse{ + Id: "lid", + DisplayName: "name", + TelemetryRouterId: "tlmrid", + CreateTime: testTime, + Status: "active", + } + for _, mod := range mods { + mod(link) + } + return link +} + +func fixtureModel(mods ...func(model *Model)) *Model { + model := &Model{ + ID: types.StringValue("rtp,rid,reg"), + LinkID: types.StringValue("lid"), + Region: types.StringValue("reg"), + ResourceType: types.StringValue("rtp"), + ResourceID: types.StringValue("rid"), + DisplayName: types.StringValue("name"), + Description: types.String{}, + TelemetryRouterID: types.StringValue("tlmrid"), + AccessToken: types.String{}, + CreateTime: types.StringValue(testTime.String()), + Status: types.StringValue("active"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *telemetrylink.TelemetryLinkResponse + expected *Model + wantErr bool + }{ + { + description: "min values", + input: fixtureLink(), + expected: fixtureModel(), + }, + { + description: "max values", + input: fixtureLink(func(link *telemetrylink.TelemetryLinkResponse) { + link.Description = new("description") + link.DisplayName = "display-name" + link.AccessToken = new("access-token") + link.TelemetryRouterId = "tlmr-id" + }), + expected: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.TelemetryRouterID = types.StringValue("tlmr-id") + }), + }, + { + description: "nil input", + wantErr: true, + expected: fixtureModel(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + state := &Model{ + ResourceType: tt.expected.ResourceType, + ResourceID: tt.expected.ResourceID, + Region: tt.expected.Region, + } + err := mapFields(context.Background(), tt.input, state) + if tt.wantErr && err == nil { + t.Fatalf("Should have failed") + } + if !tt.wantErr && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if !tt.wantErr { + diff := cmp.Diff(state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToCreateOrUpdateOrganizationTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + DisplayName: "name", + AccessToken: "", + TelemetryRouterId: "tlmrid", + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.CreateOrUpdateOrganizationTelemetryLinkPayload{ + Description: new("description"), + DisplayName: "display-name", + AccessToken: "access-token", + TelemetryRouterId: "tlmr_id", + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateOrganizationTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} + +func TestToCreateOrUpdateFolderTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + DisplayName: "name", + AccessToken: "", + TelemetryRouterId: "tlmrid", + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.CreateOrUpdateFolderTelemetryLinkPayload{ + Description: new("description"), + DisplayName: "display-name", + AccessToken: "access-token", + TelemetryRouterId: "tlmr_id", + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateFolderTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} + +func TestToCreateOrUpdateProjectTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + DisplayName: "name", + AccessToken: "", + TelemetryRouterId: "tlmrid", + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.CreateOrUpdateProjectTelemetryLinkPayload{ + Description: new("description"), + DisplayName: "display-name", + AccessToken: "access-token", + TelemetryRouterId: "tlmr_id", + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateProjectTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} + +func TestToPartialUpdateOrganizationTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + DisplayName: new("name"), + AccessToken: new(""), + TelemetryRouterId: new("tlmrid"), + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.PartialUpdateOrganizationTelemetryLinkPayload{ + Description: new("description"), + DisplayName: new("display-name"), + AccessToken: new("access-token"), + TelemetryRouterId: new("tlmr_id"), + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateOrganizationTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} + +func TestToPartialUpdateFolderTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.PartialUpdateFolderTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + DisplayName: new("name"), + AccessToken: new(""), + TelemetryRouterId: new("tlmrid"), + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.PartialUpdateFolderTelemetryLinkPayload{ + Description: new("description"), + DisplayName: new("display-name"), + AccessToken: new("access-token"), + TelemetryRouterId: new("tlmr_id"), + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateFolderTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} + +func TestToPartialUpdateProjectTelemetryLinkPayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *telemetrylink.PartialUpdateProjectTelemetryLinkPayload + wantErrMessage string + }{ + { + description: "min values", + model: fixtureModel(), + expected: &telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + DisplayName: new("name"), + AccessToken: new(""), + TelemetryRouterId: new("tlmrid"), + }, + }, + { + description: "max values", + model: fixtureModel(func(model *Model) { + model.Description = types.StringValue("description") + model.DisplayName = types.StringValue("display-name") + model.AccessToken = types.StringValue("access-token") + model.TelemetryRouterID = types.StringValue("tlmr_id") + }), + expected: &telemetrylink.PartialUpdateProjectTelemetryLinkPayload{ + Description: new("description"), + DisplayName: new("display-name"), + AccessToken: new("access-token"), + TelemetryRouterId: new("tlmr_id"), + }, + }, + { + description: "nil model", + wantErrMessage: "missing model", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got, err := toCreateOrUpdateProjectTelemetryLinkPayload(t.Context(), diag.Diagnostics{}, tt.model) + if tt.wantErrMessage != "" && (err == nil || err.Error() != tt.wantErrMessage) { + t.Fatalf("Expected error: %v, got: %v", tt.wantErrMessage, err) + } + if tt.wantErrMessage == "" && err != nil { + t.Fatalf("Unexpected error: %v", err) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Fatalf("Payload does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/telemetrylink/telemetrylink_acc_test.go b/stackit/internal/services/telemetrylink/telemetrylink_acc_test.go new file mode 100644 index 000000000..f894db58b --- /dev/null +++ b/stackit/internal/services/telemetrylink/telemetrylink_acc_test.go @@ -0,0 +1,346 @@ +package telemetrylink_test + +import ( + "context" + _ "embed" + "errors" + "fmt" + "maps" + "strings" + "sync" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +var ( + //go:embed testdata/resource-min.tf + resourceMin string + + //go:embed testdata/resource-max.tf + resourceMax string +) + +var testConfigVarsMin = config.Variables{ + "resource_type": config.StringVariable("project"), + "resource_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable(testutil.Region), + "display_name": config.StringVariable("tf-acc-test-link-min"), + "access_token": config.StringVariable("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"), + "telemetry_router_id": config.StringVariable("97272f10-87ec-4715-b280-195a4ab1856c"), +} + +func testConfigVarsMinUpdated() config.Variables { + newVars := make(config.Variables, len(testConfigVarsMin)) + maps.Copy(newVars, testConfigVarsMin) + newVars["display_name"] = config.StringVariable("tf-acc-test-link-updated") + return newVars +} + +var testConfigVarsMax = config.Variables{ + "resource_type": config.StringVariable("project"), + "resource_id": config.StringVariable(testutil.ProjectId), + "region": config.StringVariable(testutil.Region), + "display_name": config.StringVariable("tf-acc-test-link-max"), + "description": config.StringVariable("tf-acc-test-link-description"), + "access_token": config.StringVariable("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"), + "telemetry_router_id": config.StringVariable("97272f10-87ec-4715-b280-195a4ab1856c"), +} + +func testConfigVarsMaxUpdated() config.Variables { + newVars := make(config.Variables, len(testConfigVarsMin)) + maps.Copy(newVars, testConfigVarsMin) + newVars["display_name"] = config.StringVariable("tf-acc-test-link-updated") + newVars["description"] = config.StringVariable("Terraform Acceptance Test TelemetryLink Updated") + return newVars +} + +func TestAccTelemetryLinkLinkMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Create + { + ConfigVariables: testConfigVarsMin, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMin, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMin["resource_type"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMin["resource_id"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "region", testutil.ConvertConfigVariable(testConfigVarsMin["region"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "display_name", testutil.ConvertConfigVariable(testConfigVarsMin["display_name"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "access_token", testutil.ConvertConfigVariable(testConfigVarsMin["access_token"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "telemetry_router_id", testutil.ConvertConfigVariable(testConfigVarsMin["telemetry_router_id"])), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "link_id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "create_time"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "status"), + ), + }, + // Datasource + { + ConfigVariables: testConfigVarsMin, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMin + ` + data "stackit_telemetrylink" "link" { + resource_type = stackit_telemetrylink.link.resource_type + resource_id = stackit_telemetrylink.link.resource_id + region = stackit_telemetrylink.link.region + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMin["resource_type"])), + resource.TestCheckResourceAttr("data.stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMin["resource_id"])), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "region", + "data.stackit_telemetrylink.link", "region", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "id", + "data.stackit_telemetrylink.link", "id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "link_id", + "data.stackit_telemetrylink.link", "link_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "display_name", + "data.stackit_telemetrylink.link", "display_name", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "create_time", + "data.stackit_telemetrylink.link", "create_time", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "telemetry_router_id", + "data.stackit_telemetrylink.link", "telemetry_router_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "status", + "data.stackit_telemetrylink.link", "status", + ), + ), + }, + // Import + { + ConfigVariables: testConfigVarsMin, + ResourceName: "stackit_telemetrylink.link", + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, ok := state.RootModule().Resources["stackit_telemetrylink.link"] + if !ok { + return "", fmt.Errorf("not found: %s", "stackit_telemetrylink.link") + } + resourceType, ok := rs.Primary.Attributes["resource_type"] + if !ok { + return "", fmt.Errorf("resource_type not set") + } + return fmt.Sprintf("%s,%s,%s", resourceType, testutil.ProjectId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"access_token"}, + }, + // Update + { + ConfigVariables: testConfigVarsMinUpdated(), + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["resource_type"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["resource_id"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "region", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["region"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "display_name", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["display_name"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "access_token", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["access_token"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "telemetry_router_id", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["telemetry_router_id"])), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "link_id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "create_time"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "status"), + ), + }, + // Deletion handled by framework + }, + }) +} + +func TestAccTelemetryLinkLinkMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Create + { + ConfigVariables: testConfigVarsMax, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMax, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMax["resource_type"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMax["resource_id"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "region", testutil.ConvertConfigVariable(testConfigVarsMax["region"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "display_name", testutil.ConvertConfigVariable(testConfigVarsMax["display_name"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "access_token", testutil.ConvertConfigVariable(testConfigVarsMax["access_token"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "telemetry_router_id", testutil.ConvertConfigVariable(testConfigVarsMax["telemetry_router_id"])), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "link_id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "create_time"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "status"), + ), + }, + // Datasource + { + ConfigVariables: testConfigVarsMax, + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMax + ` + data "stackit_telemetrylink" "link" { + resource_type = stackit_telemetrylink.link.resource_type + resource_id = stackit_telemetrylink.link.resource_id + region = stackit_telemetrylink.link.region + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMax["resource_type"])), + resource.TestCheckResourceAttr("data.stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMax["resource_id"])), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "region", + "data.stackit_telemetrylink.link", "region", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "id", + "data.stackit_telemetrylink.link", "id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "link_id", + "data.stackit_telemetrylink.link", "link_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "display_name", + "data.stackit_telemetrylink.link", "display_name", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "description", + "data.stackit_telemetrylink.link", "description", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "create_time", + "data.stackit_telemetrylink.link", "create_time", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "telemetry_router_id", + "data.stackit_telemetrylink.link", "telemetry_router_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_telemetrylink.link", "status", + "data.stackit_telemetrylink.link", "status", + ), + ), + }, + // Import + { + ConfigVariables: testConfigVarsMax, + ResourceName: "stackit_telemetrylink.link", + ImportStateIdFunc: func(state *terraform.State) (string, error) { + rs, ok := state.RootModule().Resources["stackit_telemetrylink.link"] + if !ok { + return "", fmt.Errorf("not found: %s", "stackit_telemetrylink.link") + } + resourceType, ok := rs.Primary.Attributes["resource_type"] + if !ok { + return "", fmt.Errorf("resource_type not set") + } + return fmt.Sprintf("%s,%s,%s", resourceType, testutil.ProjectId, testutil.Region), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"access_token"}, + }, + // Update + { + ConfigVariables: testConfigVarsMaxUpdated(), + Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + resourceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_type", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["resource_type"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "resource_id", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["resource_id"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "region", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["region"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "display_name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["display_name"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "description", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["description"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "access_token", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["access_token"])), + resource.TestCheckResourceAttr("stackit_telemetrylink.link", "telemetry_router_id", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["telemetry_router_id"])), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "link_id"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "create_time"), + resource.TestCheckResourceAttrSet("stackit_telemetrylink.link", "status"), + ), + }, + // Deletion handled by framework + }, + }) +} + +func testAccCheckDestroy(s *terraform.State) error { + checkFunctions := []func(s *terraform.State) error{ + testAccCheckLogsInstanceDestroy, + } + + var errs []error + + wg := sync.WaitGroup{} + wg.Add(len(checkFunctions)) + + for _, f := range checkFunctions { + go func() { + err := f(s) + if err != nil { + errs = append(errs, err) + } + wg.Done() + }() + } + wg.Wait() + return errors.Join(errs...) +} + +func testAccCheckLogsInstanceDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := telemetrylink.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.LogsCustomEndpoint, false)...) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + type link struct { + resourceType string + resourceId string + region string + } + + var linksToDestroy []link + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_telemetrylink" { + continue + } + parts := strings.Split(rs.Primary.ID, core.Separator) + linksToDestroy = append(linksToDestroy, link{ + resourceType: parts[0], + resourceId: parts[1], + region: parts[2], + }) + } + + for _, l := range linksToDestroy { + var err error + switch l.resourceType { + case "organization": + err = client.DefaultAPI.DeleteOrganizationTelemetryLink(ctx, l.resourceId, l.region).Execute() + case "folder": + err = client.DefaultAPI.DeleteFolderTelemetryLink(ctx, l.resourceId, l.region).Execute() + case "project": + err = client.DefaultAPI.DeleteProjectTelemetryLink(ctx, l.resourceId, l.region).Execute() + } + if err != nil { + return fmt.Errorf("deleting link %s %s: %w", l.resourceType, l.resourceId, err) + } + } + return nil +} diff --git a/stackit/internal/services/telemetrylink/testdata/resource-max.tf b/stackit/internal/services/telemetrylink/testdata/resource-max.tf new file mode 100644 index 000000000..7c46035ad --- /dev/null +++ b/stackit/internal/services/telemetrylink/testdata/resource-max.tf @@ -0,0 +1,18 @@ + +variable "resource_type" {} +variable "resource_id" {} +variable "region" {} +variable "display_name" {} +variable "description" {} +variable "access_token" {} +variable "telemetry_router_id" {} + +resource "stackit_telemetrylink" "link" { + resource_type = var.resource_type + resource_id = var.resource_id + region = var.region + display_name = var.display_name + description = var.description + access_token = var.access_token + telemetry_router_id = var.telemetry_router_id +} diff --git a/stackit/internal/services/telemetrylink/testdata/resource-min.tf b/stackit/internal/services/telemetrylink/testdata/resource-min.tf new file mode 100644 index 000000000..b140056f4 --- /dev/null +++ b/stackit/internal/services/telemetrylink/testdata/resource-min.tf @@ -0,0 +1,16 @@ + +variable "resource_type" {} +variable "resource_id" {} +variable "region" {} +variable "display_name" {} +variable "access_token" {} +variable "telemetry_router_id" {} + +resource "stackit_telemetrylink" "link" { + resource_type = var.resource_type + resource_id = var.resource_id + region = var.region + display_name = var.display_name + access_token = var.access_token + telemetry_router_id = var.telemetry_router_id +} diff --git a/stackit/internal/services/telemetrylink/utils/utils.go b/stackit/internal/services/telemetrylink/utils/utils.go new file mode 100644 index 000000000..dd43ab2a6 --- /dev/null +++ b/stackit/internal/services/telemetrylink/utils/utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *telemetrylink.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.TelemetryLinkCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.TelemetryLinkCustomEndpoint)) + } + apiClient, err := telemetrylink.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} diff --git a/stackit/internal/services/telemetrylink/utils/utils_test.go b/stackit/internal/services/telemetrylink/utils/utils_test.go new file mode 100644 index 000000000..ddefaa831 --- /dev/null +++ b/stackit/internal/services/telemetrylink/utils/utils_test.go @@ -0,0 +1,94 @@ +package utils + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" + "github.com/stackitcloud/stackit-sdk-go/core/config" + telemetrylink "github.com/stackitcloud/stackit-sdk-go/services/telemetrylink/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +const ( + testVersion = "1.2.3" + testCustomEndpoint = "https://telemetrylink-custom-endpoint.api.stackit.cloud" +) + +func TestConfigureClient(t *testing.T) { + /* mock authentication by setting service account token env variable */ + os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") + if err != nil { + t.Errorf("error setting env variable: %v", err) + } + + type args struct { + providerData *core.ProviderData + } + tests := []struct { + name string + args args + wantErr bool + expected *telemetrylink.APIClient + }{ + { + name: "default endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + }, + }, + expected: func() *telemetrylink.APIClient { + apiClient, err := telemetrylink.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + { + name: "custom endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + TelemetryLinkCustomEndpoint: testCustomEndpoint, + }, + }, + expected: func() *telemetrylink.APIClient { + apiClient, err := telemetrylink.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + config.WithEndpoint(testCustomEndpoint), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + diags := diag.Diagnostics{} + + actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { + t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) + } + + if !reflect.DeepEqual(actual, tt.expected) { + t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) + } + }) + } +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 553e54774..15fe4eede 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -99,6 +99,7 @@ var ( TokenCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_TOKEN_CUSTOM_ENDPOINT", providerName: "token_custom_endpoint"} SKECustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_SKE_CUSTOM_ENDPOINT", providerName: "ske_custom_endpoint"} IntakeCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_INTAKE_CUSTOM_ENDPOINT", providerName: "intake_custom_endpoint"} + TelemetryLinkCustomEndpoint = customEndpointConfig{envVarName: "TF_ACC_TELEMETRYLINK_CUSTOM_ENDPOINT", providerName: "telemetrylink_custom_endpoint"} allCustomEndpoints = []customEndpointConfig{ ALBCustomEndpoint, @@ -132,6 +133,7 @@ var ( ServiceAccountCustomEndpoint, TokenCustomEndpoint, SKECustomEndpoint, + TelemetryLinkCustomEndpoint, } ) diff --git a/stackit/provider.go b/stackit/provider.go index 52937fd3a..03a27d3a7 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -19,6 +19,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/oidcadapters" + telemetryLink "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/link" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/access_token" @@ -199,6 +201,7 @@ type providerModel struct { SfsCustomEndpoint types.String `tfsdk:"sfs_custom_endpoint"` SkeCustomEndpoint types.String `tfsdk:"ske_custom_endpoint"` SqlServerFlexCustomEndpoint types.String `tfsdk:"sqlserverflex_custom_endpoint"` + TelemetryLinkCustomEndpoint types.String `tfsdk:"telemetrylink_custom_endpoint"` TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"` OIDCTokenRequestURL types.String `tfsdk:"oidc_request_url"` OIDCTokenRequestToken types.String `tfsdk:"oidc_request_token"` @@ -256,6 +259,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service", "service_enablement_custom_endpoint": "Custom endpoint for the Service Enablement API", "sfs_custom_endpoint": "Custom endpoint for the Stackit Filestorage API", + "telemetrylink_custom_endpoint": "Custom endpoint for the Telemetry Link service", "token_custom_endpoint": "Custom endpoint for the token API, which is used to request access tokens when using the key flow", "enable_beta_resources": "Enable beta resources. Default is false.", "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), @@ -467,6 +471,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["sfs_custom_endpoint"], }, + "telemetrylink_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["telemetrylink_custom_endpoint"], + }, "token_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["token_custom_endpoint"], @@ -554,6 +562,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.SfsCustomEndpoint, func(v string) { providerData.SfsCustomEndpoint = v }) setStringField(providerConfig.SkeCustomEndpoint, func(v string) { providerData.SKECustomEndpoint = v }) setStringField(providerConfig.SqlServerFlexCustomEndpoint, func(v string) { providerData.SQLServerFlexCustomEndpoint = v }) + setStringField(providerConfig.TelemetryLinkCustomEndpoint, func(v string) { providerData.TelemetryLinkCustomEndpoint = v }) if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { var experimentValues []string @@ -715,6 +724,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource compliancelock.NewComplianceLockDataSource, serverBackupEnable.NewServerBackupEnableDataSource, serverUpdateEnable.NewServerUpdateEnableDataSource, + telemetryLink.NewTelemetryLinkLinkDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) dataSources = append(dataSources, iamRoleBindingsV1.NewRoleBindingsDatasources()...) @@ -808,6 +818,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { compliancelock.NewComplianceLockResource, serverBackupEnable.NewServerBackupEnableResource, serverUpdateEnable.NewServerUpdateEnableResource, + telemetryLink.NewTelemetryLinkResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) resources = append(resources, customRole.NewCustomRoleResources()...)