diff --git a/CHANGELOG.md b/CHANGELOG.md index 33988980..9ef90dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Support updating `elasticstack_elasticsearch_security_api_key` when supported by the backing cluster ([#843](https://github.com/elastic/terraform-provider-elasticstack/pull/843)) - Fix validation of `throttle`, and `interval` attributes in `elasticstack_kibana_alerting_rule` allowing all Elastic duration values ([#846](https://github.com/elastic/terraform-provider-elasticstack/pull/846)) - Fix boolean setting parsing for `elasticstack_elasticsearch_indices` data source. ([#842](https://github.com/elastic/terraform-provider-elasticstack/pull/842)) +- Update all Fleet and utils/tfsdk instances of diagnostics parameters to pass by pointer instead of pass by value. Added upgrader for fleet_integration_policy v0 to handle empty string vars_json/streams_json. ([#855](https://github.com/elastic/terraform-provider-elasticstack/pull/855)) ## [0.11.9] - 2024-10-14 diff --git a/internal/fleet/enrollment_tokens/models.go b/internal/fleet/enrollment_tokens/models.go index 79fc4d25..1b3bfeb3 100644 --- a/internal/fleet/enrollment_tokens/models.go +++ b/internal/fleet/enrollment_tokens/models.go @@ -27,7 +27,7 @@ type enrollmentTokenModel struct { } func (model *enrollmentTokensModel) populateFromAPI(ctx context.Context, data []fleetapi.EnrollmentApiKey) (diags diag.Diagnostics) { - model.Tokens = utils.SliceToListType(ctx, data, getTokenType(), path.Root("tokens"), diags, newEnrollmentTokenModel) + model.Tokens = utils.SliceToListType(ctx, data, getTokenType(), path.Root("tokens"), &diags, newEnrollmentTokenModel) return } diff --git a/internal/fleet/integration/create.go b/internal/fleet/integration/create.go index e166cf45..68869cab 100644 --- a/internal/fleet/integration/create.go +++ b/internal/fleet/integration/create.go @@ -11,10 +11,10 @@ import ( ) func (r *integrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - r.create(ctx, req.Plan, &resp.State, resp.Diagnostics) + r.create(ctx, req.Plan, &resp.State, &resp.Diagnostics) } -func (r integrationResource) create(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State, respDiags diag.Diagnostics) { +func (r integrationResource) create(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State, respDiags *diag.Diagnostics) { var planModel integrationModel diags := plan.Get(ctx, &planModel) diff --git a/internal/fleet/integration/update.go b/internal/fleet/integration/update.go index 7e4bc940..28e98b1c 100644 --- a/internal/fleet/integration/update.go +++ b/internal/fleet/integration/update.go @@ -7,5 +7,5 @@ import ( ) func (r *integrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - r.create(ctx, req.Plan, &resp.State, resp.Diagnostics) + r.create(ctx, req.Plan, &resp.State, &resp.Diagnostics) } diff --git a/internal/fleet/integration_policy/models.go b/internal/fleet/integration_policy/models.go index 5dfe88c7..f2eab264 100644 --- a/internal/fleet/integration_policy/models.go +++ b/internal/fleet/integration_policy/models.go @@ -50,14 +50,14 @@ func (model *integrationPolicyModel) populateFromAPI(ctx context.Context, data * model.Enabled = types.BoolPointerValue(data.Enabled) model.IntegrationName = types.StringValue(data.Package.Name) model.IntegrationVersion = types.StringValue(data.Package.Version) - model.VarsJson = utils.MapToNormalizedType(utils.Deref(data.Vars), path.Root("vars_json"), diags) + model.VarsJson = utils.MapToNormalizedType(utils.Deref(data.Vars), path.Root("vars_json"), &diags) - model.populateInputFromAPI(ctx, data.Inputs, diags) + model.populateInputFromAPI(ctx, data.Inputs, &diags) return diags } -func (model *integrationPolicyModel) populateInputFromAPI(ctx context.Context, inputs map[string]fleetapi.PackagePolicyInput, diags diag.Diagnostics) { +func (model *integrationPolicyModel) populateInputFromAPI(ctx context.Context, inputs map[string]fleetapi.PackagePolicyInput, diags *diag.Diagnostics) { newInputs := utils.TransformMapToSlice(inputs, path.Root("input"), diags, func(inputData fleetapi.PackagePolicyInput, meta utils.MapMeta) integrationPolicyInputModel { return integrationPolicyInputModel{ @@ -68,12 +68,13 @@ func (model *integrationPolicyModel) populateInputFromAPI(ctx context.Context, i } }) if newInputs == nil { - model.Input = types.ListNull(getInputType()) + model.Input = types.ListNull(getInputTypeV1()) } else { oldInputs := utils.ListTypeAs[integrationPolicyInputModel](ctx, model.Input, path.Root("input"), diags) + sortInputs(newInputs, oldInputs) - inputList, d := types.ListValueFrom(ctx, getInputType(), newInputs) + inputList, d := types.ListValueFrom(ctx, getInputTypeV1(), newInputs) diags.Append(d...) model.Input = inputList @@ -96,19 +97,19 @@ func (model integrationPolicyModel) toAPIModel(ctx context.Context, isUpdate boo Version: model.IntegrationVersion.ValueString(), }, PolicyId: model.AgentPolicyID.ValueString(), - Vars: utils.MapRef(utils.NormalizedTypeToMap[any](model.VarsJson, path.Root("vars_json"), diags)), + Vars: utils.MapRef(utils.NormalizedTypeToMap[any](model.VarsJson, path.Root("vars_json"), &diags)), } if isUpdate { body.Id = model.ID.ValueStringPointer() } - body.Inputs = utils.MapRef(utils.ListTypeToMap(ctx, model.Input, path.Root("input"), diags, + body.Inputs = utils.MapRef(utils.ListTypeToMap(ctx, model.Input, path.Root("input"), &diags, func(inputModel integrationPolicyInputModel, meta utils.ListMeta) (string, fleetapi.PackagePolicyRequestInput) { return inputModel.InputID.ValueString(), fleetapi.PackagePolicyRequestInput{ Enabled: inputModel.Enabled.ValueBoolPointer(), - Streams: utils.MapRef(utils.NormalizedTypeToMap[fleetapi.PackagePolicyRequestInputStream](inputModel.StreamsJson, meta.Path.AtName("streams_json"), diags)), - Vars: utils.MapRef(utils.NormalizedTypeToMap[any](inputModel.VarsJson, meta.Path.AtName("vars_json"), diags)), + Streams: utils.MapRef(utils.NormalizedTypeToMap[fleetapi.PackagePolicyRequestInputStream](inputModel.StreamsJson, meta.Path.AtName("streams_json"), &diags)), + Vars: utils.MapRef(utils.NormalizedTypeToMap[any](inputModel.VarsJson, meta.Path.AtName("vars_json"), &diags)), } })) diff --git a/internal/fleet/integration_policy/resource.go b/internal/fleet/integration_policy/resource.go index e1cc39be..b10e68a8 100644 --- a/internal/fleet/integration_policy/resource.go +++ b/internal/fleet/integration_policy/resource.go @@ -10,9 +10,10 @@ import ( ) var ( - _ resource.Resource = &integrationPolicyResource{} - _ resource.ResourceWithConfigure = &integrationPolicyResource{} - _ resource.ResourceWithImportState = &integrationPolicyResource{} + _ resource.Resource = &integrationPolicyResource{} + _ resource.ResourceWithConfigure = &integrationPolicyResource{} + _ resource.ResourceWithImportState = &integrationPolicyResource{} + _ resource.ResourceWithUpgradeState = &integrationPolicyResource{} ) // NewResource is a helper function to simplify the provider implementation. @@ -37,3 +38,9 @@ func (r *integrationPolicyResource) Metadata(ctx context.Context, req resource.M func (r *integrationPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("policy_id"), req, resp) } + +func (r *integrationPolicyResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: {PriorSchema: getSchemaV0(), StateUpgrader: upgradeV0}, + } +} diff --git a/internal/fleet/integration_policy/schema.go b/internal/fleet/integration_policy/schema.go index 80afc60f..055a7061 100644 --- a/internal/fleet/integration_policy/schema.go +++ b/internal/fleet/integration_policy/schema.go @@ -13,11 +13,12 @@ import ( ) func (r *integrationPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = getSchema() + resp.Schema = getSchemaV1() } -func getSchema() schema.Schema { +func getSchemaV1() schema.Schema { return schema.Schema{ + Version: 1, Description: "Creates a new Fleet Integration Policy. See https://www.elastic.co/guide/en/fleet/current/add-integration-to-policy.html", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -114,6 +115,6 @@ func getSchema() schema.Schema { } } -func getInputType() attr.Type { - return getSchema().Blocks["input"].Type().(attr.TypeWithElementType).ElementType() +func getInputTypeV1() attr.Type { + return getSchemaV1().Blocks["input"].Type().(attr.TypeWithElementType).ElementType() } diff --git a/internal/fleet/integration_policy/upgrade.go b/internal/fleet/integration_policy/upgrade.go new file mode 100644 index 00000000..7992ce15 --- /dev/null +++ b/internal/fleet/integration_policy/upgrade.go @@ -0,0 +1,114 @@ +package integration_policy + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func getSchemaV0() *schema.Schema { + return &schema.Schema{ + Version: 0, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "policy_id": schema.StringAttribute{Computed: true, Optional: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown()}}, + "name": schema.StringAttribute{Required: true}, + "namespace": schema.StringAttribute{Required: true}, + "agent_policy_id": schema.StringAttribute{Required: true}, + "description": schema.StringAttribute{Optional: true}, + "enabled": schema.BoolAttribute{Computed: true, Optional: true, Default: booldefault.StaticBool(true)}, + "force": schema.BoolAttribute{Optional: true}, + "integration_name": schema.StringAttribute{Required: true}, + "integration_version": schema.StringAttribute{Required: true}, + "vars_json": schema.StringAttribute{Computed: true, Optional: true, Sensitive: true}, + }, + Blocks: map[string]schema.Block{ + "input": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "input_id": schema.StringAttribute{Required: true}, + "enabled": schema.BoolAttribute{Computed: true, Optional: true, Default: booldefault.StaticBool(true)}, + "streams_json": schema.StringAttribute{Computed: true, Optional: true, Sensitive: true}, + "vars_json": schema.StringAttribute{Computed: true, Optional: true, Sensitive: true}, + }, + }, + }, + }, + } +} + +func getInputTypeV0() attr.Type { + return getSchemaV0().Blocks["input"].Type().(attr.TypeWithElementType).ElementType() +} + +type integrationPolicyModelV0 struct { + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Namespace types.String `tfsdk:"namespace"` + AgentPolicyID types.String `tfsdk:"agent_policy_id"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + Force types.Bool `tfsdk:"force"` + IntegrationName types.String `tfsdk:"integration_name"` + IntegrationVersion types.String `tfsdk:"integration_version"` + Input types.List `tfsdk:"input"` //> integrationPolicyInputModelV0 + VarsJson types.String `tfsdk:"vars_json"` +} + +type integrationPolicyInputModelV0 struct { + InputID types.String `tfsdk:"input_id"` + Enabled types.Bool `tfsdk:"enabled"` + StreamsJson types.String `tfsdk:"streams_json"` + VarsJson types.String `tfsdk:"vars_json"` +} + +// The schema between V0 and V1 is mostly the same, however vars_json and +// streams_json saved "" values to the state when null values were in the +// config. jsontypes.Normalized correctly states this is invalid JSON. +func upgradeV0(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var stateModel integrationPolicyModelV0 + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if varsJSON := stateModel.VarsJson.ValueStringPointer(); varsJSON != nil { + if *varsJSON == "" { + stateModel.VarsJson = types.StringNull() + } + } + + inputs := utils.ListTypeAs[integrationPolicyInputModelV0](ctx, stateModel.Input, path.Root("input"), &resp.Diagnostics) + for index, input := range inputs { + if varsJSON := input.VarsJson.ValueStringPointer(); varsJSON != nil { + if *varsJSON == "" { + input.VarsJson = types.StringNull() + } + } + if streamsJSON := input.StreamsJson.ValueStringPointer(); streamsJSON != nil { + if *streamsJSON == "" { + input.StreamsJson = types.StringNull() + } + } + inputs[index] = input + } + + stateModel.Input = utils.ListValueFrom(ctx, inputs, getInputTypeV0(), path.Root("input"), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/output/models.go b/internal/fleet/output/models.go index afa5b866..843f4c34 100644 --- a/internal/fleet/output/models.go +++ b/internal/fleet/output/models.go @@ -49,7 +49,7 @@ func (model *outputModel) populateFromAPICreate(ctx context.Context, data *fleet model.OutputID = types.StringPointerValue(data.Id) model.Name = types.StringValue(data.Name) model.Type = types.StringValue(string(data.Type)) - model.Hosts = utils.SliceToListType_String(ctx, utils.Deref(data.Hosts), path.Root("hosts"), diags) + model.Hosts = utils.SliceToListType_String(ctx, utils.Deref(data.Hosts), path.Root("hosts"), &diags) model.CaSha256 = types.StringPointerValue(data.CaSha256) model.CaTrustedFingerprint = types.StringPointerValue(data.CaTrustedFingerprint) model.DefaultIntegrations = types.BoolPointerValue(data.IsDefault) @@ -59,7 +59,7 @@ func (model *outputModel) populateFromAPICreate(ctx context.Context, data *fleet if data.Ssl != nil { p := path.Root("ssl") sslModels := []outputSslModel{{ - CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), diags), + CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), &diags), Certificate: types.StringPointerValue(data.Ssl.Certificate), Key: types.StringPointerValue(data.Ssl.Key), }} @@ -74,7 +74,7 @@ func (model *outputModel) populateFromAPICreate(ctx context.Context, data *fleet model.OutputID = types.StringPointerValue(data.Id) model.Name = types.StringValue(data.Name) model.Type = types.StringValue(string(data.Type)) - model.Hosts = utils.SliceToListType_String(ctx, data.Hosts, path.Root("hosts"), diags) + model.Hosts = utils.SliceToListType_String(ctx, data.Hosts, path.Root("hosts"), &diags) model.CaSha256 = types.StringPointerValue(data.CaSha256) model.CaTrustedFingerprint = types.StringPointerValue(data.CaTrustedFingerprint) model.DefaultIntegrations = types.BoolPointerValue(data.IsDefault) @@ -84,7 +84,7 @@ func (model *outputModel) populateFromAPICreate(ctx context.Context, data *fleet if data.Ssl != nil { p := path.Root("ssl") sslModels := []outputSslModel{{ - CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), diags), + CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), &diags), Certificate: types.StringPointerValue(data.Ssl.Certificate), Key: types.StringPointerValue(data.Ssl.Key), }} @@ -119,7 +119,7 @@ func (model *outputModel) populateFromAPIUpdate(ctx context.Context, data *fleet model.OutputID = types.StringPointerValue(data.Id) model.Name = types.StringValue(data.Name) model.Type = types.StringValue(string(data.Type)) - model.Hosts = utils.SliceToListType_String(ctx, data.Hosts, path.Root("hosts"), diags) + model.Hosts = utils.SliceToListType_String(ctx, data.Hosts, path.Root("hosts"), &diags) model.CaSha256 = types.StringPointerValue(data.CaSha256) model.CaTrustedFingerprint = types.StringPointerValue(data.CaTrustedFingerprint) model.DefaultIntegrations = types.BoolPointerValue(data.IsDefault) @@ -129,7 +129,7 @@ func (model *outputModel) populateFromAPIUpdate(ctx context.Context, data *fleet if data.Ssl != nil { p := path.Root("ssl") sslModel := []outputSslModel{{ - CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), diags), + CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), &diags), Certificate: types.StringPointerValue(data.Ssl.Certificate), Key: types.StringPointerValue(data.Ssl.Key), }} @@ -144,7 +144,7 @@ func (model *outputModel) populateFromAPIUpdate(ctx context.Context, data *fleet model.OutputID = types.StringPointerValue(data.Id) model.Name = types.StringValue(data.Name) model.Type = types.StringValue(string(data.Type)) - model.Hosts = utils.SliceToListType_String(ctx, utils.Deref(data.Hosts), path.Root("hosts"), diags) + model.Hosts = utils.SliceToListType_String(ctx, utils.Deref(data.Hosts), path.Root("hosts"), &diags) model.CaSha256 = types.StringPointerValue(data.CaSha256) model.CaTrustedFingerprint = types.StringPointerValue(data.CaTrustedFingerprint) model.DefaultIntegrations = types.BoolPointerValue(data.IsDefault) @@ -154,7 +154,7 @@ func (model *outputModel) populateFromAPIUpdate(ctx context.Context, data *fleet if data.Ssl != nil { p := path.Root("ssl") sslModel := []outputSslModel{{ - CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), diags), + CertificateAuthorities: utils.SliceToListType_String(ctx, utils.Deref(data.Ssl.CertificateAuthorities), p.AtName("certificate_authorities"), &diags), Certificate: types.StringPointerValue(data.Ssl.Certificate), Key: types.StringPointerValue(data.Ssl.Key), }} @@ -180,7 +180,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O CaSha256: model.CaSha256.ValueStringPointer(), CaTrustedFingerprint: model.CaTrustedFingerprint.ValueStringPointer(), ConfigYaml: model.ConfigYaml.ValueStringPointer(), - Hosts: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags)), + Hosts: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags)), Id: model.OutputID.ValueStringPointer(), IsDefault: model.DefaultIntegrations.ValueBoolPointer(), IsDefaultMonitoring: model.DefaultMonitoring.ValueBoolPointer(), @@ -189,7 +189,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O // Can't use helpers for anonymous structs if utils.IsKnown(model.Ssl) { - sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), diags) + sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), &diags) if len(sslModels) > 0 { body.Ssl = &struct { Certificate *string `json:"certificate,omitempty"` @@ -197,7 +197,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O Key *string `json:"key,omitempty"` }{ Certificate: sslModels[0].Certificate.ValueStringPointer(), - CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), diags)), + CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), &diags)), Key: sslModels[0].Key.ValueStringPointer(), } } @@ -213,7 +213,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O CaSha256: model.CaSha256.ValueStringPointer(), CaTrustedFingerprint: model.CaTrustedFingerprint.ValueStringPointer(), ConfigYaml: model.ConfigYaml.ValueStringPointer(), - Hosts: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags), + Hosts: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags), Id: model.OutputID.ValueStringPointer(), IsDefault: model.DefaultIntegrations.ValueBoolPointer(), IsDefaultMonitoring: model.DefaultMonitoring.ValueBoolPointer(), @@ -223,7 +223,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O // Can't use helpers for anonymous structs if utils.IsKnown(model.Ssl) { - sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), diags) + sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), &diags) if len(sslModels) > 0 { body.Ssl = &struct { Certificate *string `json:"certificate,omitempty"` @@ -231,7 +231,7 @@ func (model outputModel) toAPICreateModel(ctx context.Context) (union fleetapi.O Key *string `json:"key,omitempty"` }{ Certificate: sslModels[0].Certificate.ValueStringPointer(), - CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), diags)), + CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), &diags)), Key: sslModels[0].Key.ValueStringPointer(), } } @@ -258,7 +258,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O CaSha256: model.CaSha256.ValueStringPointer(), CaTrustedFingerprint: model.CaTrustedFingerprint.ValueStringPointer(), ConfigYaml: model.ConfigYaml.ValueStringPointer(), - Hosts: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags), + Hosts: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags), IsDefault: model.DefaultIntegrations.ValueBoolPointer(), IsDefaultMonitoring: model.DefaultMonitoring.ValueBoolPointer(), Name: model.Name.ValueString(), @@ -266,7 +266,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O // Can't use helpers for anonymous structs if utils.IsKnown(model.Ssl) { - sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), diags) + sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), &diags) if len(sslModels) > 0 { body.Ssl = &struct { Certificate *string `json:"certificate,omitempty"` @@ -274,7 +274,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O Key *string `json:"key,omitempty"` }{ Certificate: sslModels[0].Certificate.ValueStringPointer(), - CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), diags)), + CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), &diags)), Key: sslModels[0].Key.ValueStringPointer(), } } @@ -290,7 +290,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O CaSha256: model.CaSha256.ValueStringPointer(), CaTrustedFingerprint: model.CaTrustedFingerprint.ValueStringPointer(), ConfigYaml: model.ConfigYaml.ValueStringPointer(), - Hosts: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags)), + Hosts: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags)), IsDefault: model.DefaultIntegrations.ValueBoolPointer(), IsDefaultMonitoring: model.DefaultMonitoring.ValueBoolPointer(), Name: model.Name.ValueString(), @@ -299,7 +299,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O // Can't use helpers for anonymous structs if utils.IsKnown(model.Ssl) { - sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), diags) + sslModels := utils.ListTypeAs[outputSslModel](ctx, model.Ssl, path.Root("ssl"), &diags) if len(sslModels) > 0 { body.Ssl = &struct { Certificate *string `json:"certificate,omitempty"` @@ -307,7 +307,7 @@ func (model outputModel) toAPIUpdateModel(ctx context.Context) (union fleetapi.O Key *string `json:"key,omitempty"` }{ Certificate: sslModels[0].Certificate.ValueStringPointer(), - CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), diags)), + CertificateAuthorities: utils.SliceRef(utils.ListTypeToSlice_String(ctx, sslModels[0].CertificateAuthorities, path.Root("certificate_authorities"), &diags)), Key: sslModels[0].Key.ValueStringPointer(), } } diff --git a/internal/fleet/server_host/models.go b/internal/fleet/server_host/models.go index cd2ce6e5..9788d7c6 100644 --- a/internal/fleet/server_host/models.go +++ b/internal/fleet/server_host/models.go @@ -26,7 +26,7 @@ func (model *serverHostModel) populateFromAPI(ctx context.Context, data *fleetap model.Id = types.StringValue(data.Id) model.HostID = types.StringValue(data.Id) model.Name = types.StringPointerValue(data.Name) - model.Hosts = utils.SliceToListType_String(ctx, data.HostUrls, path.Root("hosts"), diags) + model.Hosts = utils.SliceToListType_String(ctx, data.HostUrls, path.Root("hosts"), &diags) model.Default = types.BoolValue(data.IsDefault) return @@ -34,7 +34,7 @@ func (model *serverHostModel) populateFromAPI(ctx context.Context, data *fleetap func (model serverHostModel) toAPICreateModel(ctx context.Context) (body fleetapi.PostFleetServerHostsJSONRequestBody, diags diag.Diagnostics) { body = fleetapi.PostFleetServerHostsJSONRequestBody{ - HostUrls: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags), + HostUrls: utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags), Id: model.HostID.ValueStringPointer(), IsDefault: model.Default.ValueBoolPointer(), Name: model.Name.ValueString(), @@ -44,7 +44,7 @@ func (model serverHostModel) toAPICreateModel(ctx context.Context) (body fleetap func (model serverHostModel) toAPIUpdateModel(ctx context.Context) (body fleetapi.UpdateFleetServerHostsJSONRequestBody, diags diag.Diagnostics) { body = fleetapi.UpdateFleetServerHostsJSONRequestBody{ - HostUrls: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags)), + HostUrls: utils.SliceRef(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), &diags)), IsDefault: model.Default.ValueBoolPointer(), Name: model.Name.ValueStringPointer(), } diff --git a/internal/kibana/synthetics/schema.go b/internal/kibana/synthetics/schema.go index 7224edb7..8fc9c0fa 100644 --- a/internal/kibana/synthetics/schema.go +++ b/internal/kibana/synthetics/schema.go @@ -588,7 +588,7 @@ func (v *tfTCPMonitorFieldsV0) toTfTCPMonitorFieldsV0(ctx context.Context, dg di if api.CheckReceive != "" { checkReceive = types.StringValue(api.CheckReceive) } - sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root("tcp").AtName("ssl_supported_protocols"), dg) + sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root("tcp").AtName("ssl_supported_protocols"), &dg) if dg.HasError() { return nil } @@ -671,7 +671,7 @@ func (v *tfHTTPMonitorFieldsV0) toTfHTTPMonitorFieldsV0(ctx context.Context, dg return nil } - sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root("http").AtName("ssl_supported_protocols"), dg) + sslSupportedProtocols := utils.SliceToListType_String(ctx, api.SslSupportedProtocols, path.Root("http").AtName("ssl_supported_protocols"), &dg) if dg.HasError() { return nil @@ -795,7 +795,7 @@ func (v *tfModelV0) toHttpMonitorFields(ctx context.Context) (kbapi.MonitorField return nil, dg } - sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.HTTP.SslSupportedProtocols, path.Root("http").AtName("ssl_supported_protocols"), dg) + sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.HTTP.SslSupportedProtocols, path.Root("http").AtName("ssl_supported_protocols"), &dg) if dg.HasError() { return nil, dg } @@ -820,7 +820,7 @@ func (v *tfModelV0) toHttpMonitorFields(ctx context.Context) (kbapi.MonitorField func (v *tfModelV0) toTCPMonitorFields(ctx context.Context) (kbapi.MonitorFields, diag.Diagnostics) { dg := diag.Diagnostics{} - sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.TCP.SslSupportedProtocols, path.Root("tcp").AtName("ssl_supported_protocols"), dg) + sslSupportedProtocols := utils.ListTypeToSlice_String(ctx, v.TCP.SslSupportedProtocols, path.Root("tcp").AtName("ssl_supported_protocols"), &dg) if dg.HasError() { return nil, dg } diff --git a/internal/utils/tfsdk.go b/internal/utils/tfsdk.go index cdb63c9d..18c3e458 100644 --- a/internal/utils/tfsdk.go +++ b/internal/utils/tfsdk.go @@ -14,17 +14,17 @@ import ( type ListMeta struct { Index int Path path.Path - Diags diag.Diagnostics + Diags *diag.Diagnostics } type MapMeta struct { Key string Path path.Path - Diags diag.Diagnostics + Diags *diag.Diagnostics } // MapToNormalizedType marshals a map into a jsontypes.Normalized. -func MapToNormalizedType[T any](value map[string]T, p path.Path, diags diag.Diagnostics) jsontypes.Normalized { +func MapToNormalizedType[T any](value map[string]T, p path.Path, diags *diag.Diagnostics) jsontypes.Normalized { if value == nil { return jsontypes.NewNormalizedNull() } @@ -39,7 +39,7 @@ func MapToNormalizedType[T any](value map[string]T, p path.Path, diags diag.Diag // SliceToListType converts a tfsdk naive []T1 into an types.List of []T2. // This handles both structs and simple types to attr.Values. -func SliceToListType[T1 any, T2 any](ctx context.Context, value []T1, elemType attr.Type, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) types.List { +func SliceToListType[T1 any, T2 any](ctx context.Context, value []T1, elemType attr.Type, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) types.List { if value == nil { return types.ListNull(elemType) } @@ -53,7 +53,7 @@ func SliceToListType[T1 any, T2 any](ctx context.Context, value []T1, elemType a // SliceToListType_String converts a tfsdk naive []string into a types.List. // This is a shorthand SliceToListType helper for strings. -func SliceToListType_String(ctx context.Context, value []string, p path.Path, diags diag.Diagnostics) types.List { +func SliceToListType_String(ctx context.Context, value []string, p path.Path, diags *diag.Diagnostics) types.List { return SliceToListType(ctx, value, types.StringType, p, diags, func(item string, meta ListMeta) types.String { return types.StringValue(item) @@ -62,7 +62,7 @@ func SliceToListType_String(ctx context.Context, value []string, p path.Path, di // ListTypeToMap converts a types.List first into a tfsdk aware map[string]T1 // and transforms the result into a map[string]T2. -func ListTypeToMap[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { +func ListTypeToMap[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { if !IsKnown(value) { return nil } @@ -77,7 +77,7 @@ func ListTypeToMap[T1 any, T2 any](ctx context.Context, value types.List, p path // ListTypeToSlice converts a types.List first into a tfsdk aware []T1 and transforms // the result into a []T2. -func ListTypeToSlice[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { +func ListTypeToSlice[T1 any, T2 any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { if !IsKnown(value) { return nil } @@ -92,14 +92,14 @@ func ListTypeToSlice[T1 any, T2 any](ctx context.Context, value types.List, p pa // ListTypeToSlice_String converts a types.List into a []string. // This is a shorthand ListTypeToSlice helper for strings. -func ListTypeToSlice_String(ctx context.Context, value types.List, p path.Path, diags diag.Diagnostics) []string { +func ListTypeToSlice_String(ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics) []string { return ListTypeToSlice(ctx, value, p, diags, func(item types.String, meta ListMeta) string { return item.ValueString() }) } // ListTypeAs converts a types.List into a tfsdk aware []T. -func ListTypeAs[T any](ctx context.Context, value types.List, p path.Path, diags diag.Diagnostics) []T { +func ListTypeAs[T any](ctx context.Context, value types.List, p path.Path, diags *diag.Diagnostics) []T { if !IsKnown(value) { return nil } @@ -110,8 +110,15 @@ func ListTypeAs[T any](ctx context.Context, value types.List, p path.Path, diags return items } +// ListValueFrom converts a tfsdk aware []T to a types.List. +func ListValueFrom[T any](ctx context.Context, value []T, elemType attr.Type, p path.Path, diags *diag.Diagnostics) types.List { + list, d := types.ListValueFrom(ctx, elemType, value) + diags.Append(ConvertToAttrDiags(d, p)...) + return list +} + // NormalizedTypeToMap unmarshals a jsontypes.Normalized to a map[string]T. -func NormalizedTypeToMap[T any](value jsontypes.Normalized, p path.Path, diags diag.Diagnostics) map[string]T { +func NormalizedTypeToMap[T any](value jsontypes.Normalized, p path.Path, diags *diag.Diagnostics) map[string]T { if !IsKnown(value) { return nil } @@ -123,7 +130,7 @@ func NormalizedTypeToMap[T any](value jsontypes.Normalized, p path.Path, diags d } // TransformSlice converts []T1 to []T2 via the iteratee. -func TransformSlice[T1 any, T2 any](value []T1, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { +func TransformSlice[T1 any, T2 any](value []T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) T2) []T2 { if value == nil { return nil } @@ -137,7 +144,7 @@ func TransformSlice[T1 any, T2 any](value []T1, p path.Path, diags diag.Diagnost } // TransformSliceToMap converts []T1 to map[string]]T2 via the iteratee. -func TransformSliceToMap[T1 any, T2 any](value []T1, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { +func TransformSliceToMap[T1 any, T2 any](value []T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta ListMeta) (key string, elem T2)) map[string]T2 { if value == nil { return nil } @@ -152,7 +159,7 @@ func TransformSliceToMap[T1 any, T2 any](value []T1, p path.Path, diags diag.Dia } // TransformSliceToMap converts []T1 to map[string]]T2 via the iteratee. -func TransformMapToSlice[T1 any, T2 any](value map[string]T1, p path.Path, diags diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) []T2 { +func TransformMapToSlice[T1 any, T2 any](value map[string]T1, p path.Path, diags *diag.Diagnostics, iteratee func(item T1, meta MapMeta) T2) []T2 { if value == nil { return nil } diff --git a/internal/utils/tfsdk_test.go b/internal/utils/tfsdk_test.go index 19f1a972..5d7d59b4 100644 --- a/internal/utils/tfsdk_test.go +++ b/internal/utils/tfsdk_test.go @@ -95,7 +95,7 @@ func TestMapToNormalizedType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.MapToNormalizedType(tt.input, path.Empty(), diags) + got := utils.MapToNormalizedType(tt.input, path.Empty(), &diags) if !got.Equal(tt.want) { t.Errorf("MapToNormalizedType() = %v, want %v", got, tt.want) } @@ -124,7 +124,7 @@ func TestSliceToListType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.SliceToListType(ctx, tt.input, awareType, path.Empty(), diags, + got := utils.SliceToListType(ctx, tt.input, awareType, path.Empty(), &diags, func(item naive, meta utils.ListMeta) aware { return aware{ID: types.StringValue(item.ID)} }, @@ -157,7 +157,7 @@ func TestSliceToListType_String(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.SliceToListType_String(ctx, tt.input, path.Empty(), diags) + got := utils.SliceToListType_String(ctx, tt.input, path.Empty(), &diags) if !got.Equal(tt.want) { t.Errorf("SliceToListType_String() = %v, want %v", got, tt.want) } @@ -187,7 +187,7 @@ func TestListTypeToMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.ListTypeToMap(ctx, tt.input, path.Empty(), diags, + got := utils.ListTypeToMap(ctx, tt.input, path.Empty(), &diags, func(item aware, meta utils.ListMeta) (string, naive) { return "k" + item.ID.ValueString()[2:], toNaive(item) }) @@ -220,7 +220,7 @@ func TestListTypeToSlice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.ListTypeToSlice(ctx, tt.input, path.Empty(), diags, + got := utils.ListTypeToSlice(ctx, tt.input, path.Empty(), &diags, func(item aware, meta utils.ListMeta) naive { return toNaive(item) }) @@ -253,7 +253,7 @@ func TestListTypeToSlice_String(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.ListTypeToSlice_String(ctx, tt.input, path.Empty(), diags) + got := utils.ListTypeToSlice_String(ctx, tt.input, path.Empty(), &diags) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ListTypeToSlice_String() = %v, want %v", got, tt.want) } @@ -283,7 +283,7 @@ func TestListTypeAs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.ListTypeAs[aware](ctx, tt.input, path.Empty(), diags) + got := utils.ListTypeAs[aware](ctx, tt.input, path.Empty(), &diags) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ListTypeAs() = %v, want %v", got, tt.want) } @@ -294,6 +294,35 @@ func TestListTypeAs(t *testing.T) { } } +func TestListValueFrom(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []aware + want types.List + }{ + {name: "converts nil", input: awareNil, want: awareListNil}, + {name: "converts empty", input: awareEmpty, want: awareListEmpty}, + {name: "converts struct", input: awareFull, want: awareListFull}, + } + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var diags diag.Diagnostics + got := utils.ListValueFrom(ctx, tt.input, awareType, path.Empty(), &diags) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ListValueFrom() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("ListTypeAs() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) + } +} + func TestNormalizedTypeToMap(t *testing.T) { t.Parallel() @@ -311,7 +340,7 @@ func TestNormalizedTypeToMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.NormalizedTypeToMap[naive](tt.input, path.Empty(), diags) + got := utils.NormalizedTypeToMap[naive](tt.input, path.Empty(), &diags) if !reflect.DeepEqual(got, tt.want) { t.Errorf("MapToNormalizedType() = %v, want %v", got, tt.want) } @@ -338,7 +367,7 @@ func TestTransformSlice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.TransformSlice(tt.input, path.Empty(), diags, + got := utils.TransformSlice(tt.input, path.Empty(), &diags, func(item naive, meta utils.ListMeta) aware { return toAware(item) }) @@ -368,7 +397,7 @@ func TestTransformSliceToMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.TransformSliceToMap(tt.input, path.Empty(), diags, + got := utils.TransformSliceToMap(tt.input, path.Empty(), &diags, func(item aware, meta utils.ListMeta) (string, naive) { return "k" + item.ID.ValueString()[2:], toNaive(item) }) @@ -404,7 +433,7 @@ func TestTransformMapToSlice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var diags diag.Diagnostics - got := utils.TransformMapToSlice(tt.input, path.Empty(), diags, + got := utils.TransformMapToSlice(tt.input, path.Empty(), &diags, func(item naive, meta utils.MapMeta) naive { return item })