diff --git a/CHANGELOG.md b/CHANGELOG.md index 443155405..d0571c3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Prevent a provider panic when the repository referenced in an `elasticstack_elasticsearch_snapshot_repository` does not exist ([#758](https://github.com/elastic/terraform-provider-elasticstack/pull/758)) - Add support for `remote_indicies` to `elasticstack_elasticsearch_security_api_key` (#766)[https://github.com/elastic/terraform-provider-elasticstack/pull/766] - Add support for `icmp` and `browser` monitor types to `elasticstack_kibana_synthetics_monitor` resource (#772)[https://github.com/elastic/terraform-provider-elasticstack/pull/772] +- Migrate `elasticstack_fleet_enrollment_tokens` to terraform-plugin-framework ([#778](https://github.com/elastic/terraform-provider-elasticstack/pull/778)) ## [0.11.6] - 2024-08-20 diff --git a/docs/data-sources/fleet_enrollment_tokens.md b/docs/data-sources/fleet_enrollment_tokens.md index 856789204..215ba3621 100644 --- a/docs/data-sources/fleet_enrollment_tokens.md +++ b/docs/data-sources/fleet_enrollment_tokens.md @@ -32,17 +32,17 @@ data "elasticstack_fleet_enrollment_tokens" "test" { ### Read-Only - `id` (String) The ID of this resource. -- `tokens` (List of Object) A list of enrollment tokens. (see [below for nested schema](#nestedatt--tokens)) +- `tokens` (Attributes List) A list of enrollment tokens. (see [below for nested schema](#nestedatt--tokens)) ### Nested Schema for `tokens` Read-Only: -- `active` (Boolean) -- `api_key` (String) -- `api_key_id` (String) -- `created_at` (String) -- `key_id` (String) -- `name` (String) -- `policy_id` (String) +- `active` (Boolean) Indicates if the enrollment token is active. +- `api_key` (String, Sensitive) The API key. +- `api_key_id` (String) The API key identifier. +- `created_at` (String) The time at which the enrollment token was created. +- `key_id` (String) The unique identifier of the enrollment token. +- `name` (String) The name of the enrollment token. +- `policy_id` (String) The identifier of the associated agent policy. diff --git a/internal/clients/api_client.go b/internal/clients/api_client.go index 9e990a1fb..3f9865d9d 100644 --- a/internal/clients/api_client.go +++ b/internal/clients/api_client.go @@ -154,6 +154,12 @@ func ConvertProviderData(providerData any) (*ApiClient, fwdiags.Diagnostics) { return nil, diags } + if client == nil { + diags.AddError( + "Unconfigured Client", + "Expected configured client. Please report this issue to the provider developers.", + ) + } return client, diags } diff --git a/internal/clients/fleet/fleet.go b/internal/clients/fleet/fleet.go index 0bf11ee4e..e076123c4 100644 --- a/internal/clients/fleet/fleet.go +++ b/internal/clients/fleet/fleet.go @@ -8,6 +8,7 @@ import ( "net/http" fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" + fwdiag "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" ) @@ -16,20 +17,20 @@ var ( ) // AllEnrollmentTokens reads all enrollment tokens from the API. -func AllEnrollmentTokens(ctx context.Context, client *Client) ([]fleetapi.EnrollmentApiKey, diag.Diagnostics) { +func AllEnrollmentTokens(ctx context.Context, client *Client) ([]fleetapi.EnrollmentApiKey, fwdiag.Diagnostics) { resp, err := client.API.GetEnrollmentApiKeysWithResponse(ctx) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } if resp.StatusCode() == http.StatusOK { return resp.JSON200.Items, nil } - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } // GetEnrollmentTokensByPolicy Get enrollment tokens by given policy ID -func GetEnrollmentTokensByPolicy(ctx context.Context, client *Client, policyID string) ([]fleetapi.EnrollmentApiKey, diag.Diagnostics) { +func GetEnrollmentTokensByPolicy(ctx context.Context, client *Client, policyID string) ([]fleetapi.EnrollmentApiKey, fwdiag.Diagnostics) { resp, err := client.API.GetEnrollmentApiKeysWithResponse(ctx, func(ctx context.Context, req *http.Request) error { q := req.URL.Query() q.Set("kuery", "policy_id:"+policyID) @@ -38,13 +39,13 @@ func GetEnrollmentTokensByPolicy(ctx context.Context, client *Client, policyID s return nil }) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } if resp.StatusCode() == http.StatusOK { return resp.JSON200.Items, nil } - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } // ReadAgentPolicy reads a specific agent policy from the API. @@ -416,6 +417,16 @@ func AllPackages(ctx context.Context, client *Client, prerelease bool) ([]fleeta } } +// fromErr recreates the sdkdiag.FromErr functionality. +func fromErr(err error) fwdiag.Diagnostics { + if err == nil { + return nil + } + return fwdiag.Diagnostics{ + fwdiag.NewErrorDiagnostic(err.Error(), ""), + } +} + func reportUnknownError(statusCode int, body []byte) diag.Diagnostics { return diag.Diagnostics{ diag.Diagnostic{ @@ -425,3 +436,12 @@ func reportUnknownError(statusCode int, body []byte) diag.Diagnostics { }, } } + +func reportUnknownErrorFw(statusCode int, body []byte) fwdiag.Diagnostics { + return fwdiag.Diagnostics{ + fwdiag.NewErrorDiagnostic( + fmt.Sprintf("Unexpected status code from server: got HTTP %d", statusCode), + string(body), + ), + } +} diff --git a/internal/elasticsearch/index/index/create.go b/internal/elasticsearch/index/index/create.go index e2799d2ef..5e8aa652f 100644 --- a/internal/elasticsearch/index/index/create.go +++ b/internal/elasticsearch/index/index/create.go @@ -16,10 +16,6 @@ import ( var includeTypeNameMinUnsupportedVersion = version.Must(version.NewVersion("8.0.0")) func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - if !r.resourceReady(&resp.Diagnostics) { - return - } - var planModel tfModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { diff --git a/internal/elasticsearch/index/index/delete.go b/internal/elasticsearch/index/index/delete.go index bb11c4ddc..bcccb5d47 100644 --- a/internal/elasticsearch/index/index/delete.go +++ b/internal/elasticsearch/index/index/delete.go @@ -10,10 +10,6 @@ import ( ) func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - if !r.resourceReady(&resp.Diagnostics) { - return - } - var model tfModel resp.Diagnostics.Append(req.State.Get(ctx, &model)...) if resp.Diagnostics.HasError() { diff --git a/internal/elasticsearch/index/index/read.go b/internal/elasticsearch/index/index/read.go index 28e72a429..8114d5afa 100644 --- a/internal/elasticsearch/index/index/read.go +++ b/internal/elasticsearch/index/index/read.go @@ -10,10 +10,6 @@ import ( ) func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - if !r.resourceReady(&resp.Diagnostics) { - return - } - var stateModel tfModel resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) if resp.Diagnostics.HasError() { diff --git a/internal/elasticsearch/index/index/resource.go b/internal/elasticsearch/index/index/resource.go index 964919bad..439a0fa23 100644 --- a/internal/elasticsearch/index/index/resource.go +++ b/internal/elasticsearch/index/index/resource.go @@ -4,7 +4,6 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/internal/clients" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -18,18 +17,6 @@ type Resource struct { client *clients.ApiClient } -func (r *Resource) resourceReady(dg *diag.Diagnostics) bool { - if r.client == nil { - dg.AddError( - "Unconfigured Client", - "Expected configured client. Please report this issue to the provider developers.", - ) - - return false - } - return true -} - func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { client, diags := clients.ConvertProviderData(request.ProviderData) response.Diagnostics.Append(diags...) diff --git a/internal/elasticsearch/index/index/update.go b/internal/elasticsearch/index/index/update.go index 26caaf8ca..639da8ac1 100644 --- a/internal/elasticsearch/index/index/update.go +++ b/internal/elasticsearch/index/index/update.go @@ -15,10 +15,6 @@ import ( ) func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - if !r.resourceReady(&resp.Diagnostics) { - return - } - var planModel tfModel var stateModel tfModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) diff --git a/internal/fleet/enrollment_tokens/data_source.go b/internal/fleet/enrollment_tokens/data_source.go new file mode 100644 index 000000000..8af7ee9b9 --- /dev/null +++ b/internal/fleet/enrollment_tokens/data_source.go @@ -0,0 +1,33 @@ +package enrollment_tokens + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +var ( + _ datasource.DataSource = &enrollmentTokensDataSource{} + _ datasource.DataSourceWithConfigure = &enrollmentTokensDataSource{} +) + +// NewDataSource is a helper function to simplify the provider implementation. +func NewDataSource() datasource.DataSource { + return &enrollmentTokensDataSource{} +} + +type enrollmentTokensDataSource struct { + client *clients.ApiClient +} + +func (d *enrollmentTokensDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "fleet_enrollment_tokens") +} + +func (d *enrollmentTokensDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + d.client = client +} diff --git a/internal/fleet/enrollment_tokens_data_source_test.go b/internal/fleet/enrollment_tokens/data_source_test.go similarity index 61% rename from internal/fleet/enrollment_tokens_data_source_test.go rename to internal/fleet/enrollment_tokens/data_source_test.go index d7c1616d6..bc897c198 100644 --- a/internal/fleet/enrollment_tokens_data_source_test.go +++ b/internal/fleet/enrollment_tokens/data_source_test.go @@ -1,12 +1,18 @@ -package fleet_test +package enrollment_tokens_test import ( + "context" + "fmt" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) var minVersionEnrollmentTokens = version.Must(version.NewVersion("8.6.0")) @@ -46,3 +52,29 @@ data "elasticstack_fleet_enrollment_tokens" "test" { policy_id = elasticstack_fleet_agent_policy.test.policy_id } ` + +func checkResourceAgentPolicyDestroy(s *terraform.State) error { + client, err := clients.NewAcceptanceTestingClient() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "elasticstack_fleet_agent_policy" { + continue + } + + fleetClient, err := client.GetFleetClient() + if err != nil { + return err + } + policy, diags := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) + if diags.HasError() { + return utils.SdkDiagsAsError(diags) + } + if policy != nil { + return fmt.Errorf("agent policy id=%v still exists, but it should have been removed", rs.Primary.ID) + } + } + return nil +} diff --git a/internal/fleet/enrollment_tokens/models.go b/internal/fleet/enrollment_tokens/models.go new file mode 100644 index 000000000..847d9d00b --- /dev/null +++ b/internal/fleet/enrollment_tokens/models.go @@ -0,0 +1,44 @@ +package enrollment_tokens + +import ( + "context" + + fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type enrollmentTokensModel struct { + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + Tokens types.List `tfsdk:"tokens"` //> enrollmentTokenModel +} + +type enrollmentTokenModel struct { + KeyID types.String `tfsdk:"key_id"` + ApiKey types.String `tfsdk:"api_key"` + ApiKeyID types.String `tfsdk:"api_key_id"` + CreatedAt types.String `tfsdk:"created_at"` + Name types.String `tfsdk:"name"` + Active types.Bool `tfsdk:"active"` + PolicyID types.String `tfsdk:"policy_id"` +} + +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) + return +} + +func newEnrollmentTokenModel(data fleetapi.EnrollmentApiKey) enrollmentTokenModel { + return enrollmentTokenModel{ + KeyID: types.StringValue(data.Id), + Active: types.BoolValue(data.Active), + ApiKey: types.StringValue(data.ApiKey), + ApiKeyID: types.StringValue(data.ApiKeyId), + CreatedAt: types.StringValue(data.CreatedAt), + Name: types.StringPointerValue(data.Name), + PolicyID: types.StringPointerValue(data.PolicyId), + } +} diff --git a/internal/fleet/enrollment_tokens/read.go b/internal/fleet/enrollment_tokens/read.go new file mode 100644 index 000000000..a35798c85 --- /dev/null +++ b/internal/fleet/enrollment_tokens/read.go @@ -0,0 +1,59 @@ +package enrollment_tokens + +import ( + "context" + + fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (d *enrollmentTokensDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model enrollmentTokensModel + + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := d.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + var tokens []fleetapi.EnrollmentApiKey + policyID := model.PolicyID.ValueString() + if policyID == "" { + tokens, diags = fleet.AllEnrollmentTokens(ctx, client) + } else { + tokens, diags = fleet.GetEnrollmentTokensByPolicy(ctx, client, policyID) + } + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if policyID != "" { + model.ID = types.StringValue(policyID) + } else { + hash, err := utils.StringToHash(client.URL) + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + model.ID = types.StringPointerValue(hash) + } + + diags = model.populateFromAPI(ctx, tokens) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/enrollment_tokens/schema.go b/internal/fleet/enrollment_tokens/schema.go new file mode 100644 index 000000000..4a08bc154 --- /dev/null +++ b/internal/fleet/enrollment_tokens/schema.go @@ -0,0 +1,70 @@ +package enrollment_tokens + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func (d *enrollmentTokensDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = getSchema() +} + +func getSchema() schema.Schema { + return schema.Schema{ + Description: "Retrieves Elasticsearch API keys used to enroll Elastic Agents in Fleet. See: https://www.elastic.co/guide/en/fleet/current/fleet-enrollment-tokens.html", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "policy_id": schema.StringAttribute{ + Description: "The identifier of the target agent policy. When provided, only the enrollment tokens associated with this agent policy will be selected. Omit this value to select all enrollment tokens.", + Optional: true, + }, + "tokens": schema.ListNestedAttribute{ + Description: "A list of enrollment tokens.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "key_id": schema.StringAttribute{ + Description: "The unique identifier of the enrollment token.", + Computed: true, + }, + "api_key": schema.StringAttribute{ + Description: "The API key.", + Computed: true, + Sensitive: true, + }, + "api_key_id": schema.StringAttribute{ + Description: "The API key identifier.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The time at which the enrollment token was created.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the enrollment token.", + Computed: true, + }, + "active": schema.BoolAttribute{ + Description: "Indicates if the enrollment token is active.", + Computed: true, + }, + "policy_id": schema.StringAttribute{ + Description: "The identifier of the associated agent policy.", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func getTokenType() attr.Type { + return getSchema().Attributes["tokens"].GetType().(attr.TypeWithElementType).ElementType() +} diff --git a/internal/fleet/enrollment_tokens_data_source.go b/internal/fleet/enrollment_tokens_data_source.go deleted file mode 100644 index a0790637c..000000000 --- a/internal/fleet/enrollment_tokens_data_source.go +++ /dev/null @@ -1,138 +0,0 @@ -package fleet - -import ( - "context" - - fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" - "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func DataSourceEnrollmentTokens() *schema.Resource { - enrollmentTokenSchema := map[string]*schema.Schema{ - "policy_id": { - Description: "The identifier of the target agent policy. When provided, only the enrollment tokens associated with this agent policy will be selected. Omit this value to select all enrollment tokens.", - Type: schema.TypeString, - Optional: true, - }, - "tokens": { - Description: "A list of enrollment tokens.", - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key_id": { - Description: "The unique identifier of the enrollment token.", - Type: schema.TypeString, - Computed: true, - }, - "api_key": { - Description: "The API key.", - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - "api_key_id": { - Description: "The API key identifier.", - Type: schema.TypeString, - Computed: true, - }, - "created_at": { - Description: "The time at which the enrollment token was created.", - Type: schema.TypeString, - Computed: true, - }, - "name": { - Description: "The name of the enrollment token.", - Type: schema.TypeString, - Computed: true, - }, - "active": { - Description: "Indicates if the enrollment token is active.", - Type: schema.TypeBool, - Computed: true, - }, - "policy_id": { - Description: "The identifier of the associated agent policy.", - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - } - - return &schema.Resource{ - Description: "Retrieves Elasticsearch API keys used to enroll Elastic Agents in Fleet. See: https://www.elastic.co/guide/en/fleet/current/fleet-enrollment-tokens.html", - - ReadContext: dataSourceEnrollmentTokensRead, - - Schema: enrollmentTokenSchema, - } -} - -func dataSourceEnrollmentTokensRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - if d.Id() == "" { - d.SetId(d.Get("policy_id").(string)) - } - policyID := d.Id() - - var allTokens []fleetapi.EnrollmentApiKey - - if policyID == "" { - allTokens, diags = fleet.AllEnrollmentTokens(ctx, fleetClient) - } else { - allTokens, diags = fleet.GetEnrollmentTokensByPolicy(ctx, fleetClient, policyID) - } - - if diags.HasError() { - return diags - } - - var enrollmentTokens []map[string]any - for _, v := range allTokens { - if policyID != "" && v.PolicyId != nil && *v.PolicyId != policyID { - continue - } - - keyData := map[string]any{ - "api_key": v.ApiKey, - "api_key_id": v.ApiKeyId, - "created_at": v.CreatedAt, - "active": v.Active, - } - if v.Name != nil { - keyData["name"] = *v.Name - } - if v.PolicyId != nil { - keyData["policy_id"] = *v.PolicyId - } - - enrollmentTokens = append(enrollmentTokens, keyData) - } - - if enrollmentTokens != nil { - if err := d.Set("tokens", enrollmentTokens); err != nil { - return diag.FromErr(err) - } - } - - if policyID != "" { - d.SetId(policyID) - } else { - hash, err := utils.StringToHash(fleetClient.URL) - if err != nil { - return diag.FromErr(err) - } - d.SetId(*hash) - } - - return diags -} diff --git a/internal/kibana/data_view/create.go b/internal/kibana/data_view/create.go index 8c99f3be9..27e715797 100644 --- a/internal/kibana/data_view/create.go +++ b/internal/kibana/data_view/create.go @@ -10,10 +10,6 @@ import ( ) func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - if !r.resourceReady(&response.Diagnostics) { - return - } - dataviewClient, err := r.client.GetDataViewsClient() if err != nil { response.Diagnostics.AddError("unable to get data view client", err.Error()) diff --git a/internal/kibana/data_view/delete.go b/internal/kibana/data_view/delete.go index a30d20128..445bb917e 100644 --- a/internal/kibana/data_view/delete.go +++ b/internal/kibana/data_view/delete.go @@ -8,10 +8,6 @@ import ( ) func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { - if !r.resourceReady(&response.Diagnostics) { - return - } - dataviewClient, err := r.client.GetDataViewsClient() if err != nil { response.Diagnostics.AddError("unable to get data view client", err.Error()) diff --git a/internal/kibana/data_view/read.go b/internal/kibana/data_view/read.go index dafe9def7..d94d5ad3c 100644 --- a/internal/kibana/data_view/read.go +++ b/internal/kibana/data_view/read.go @@ -9,10 +9,6 @@ import ( ) func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { - if !r.resourceReady(&response.Diagnostics) { - return - } - var model tfModelV0 response.Diagnostics.Append(request.State.Get(ctx, &model)...) if response.Diagnostics.HasError() { diff --git a/internal/kibana/data_view/schema.go b/internal/kibana/data_view/schema.go index 36a4b2d43..fe953264b 100644 --- a/internal/kibana/data_view/schema.go +++ b/internal/kibana/data_view/schema.go @@ -179,18 +179,6 @@ type Resource struct { client *clients.ApiClient } -func (r *Resource) resourceReady(dg *diag.Diagnostics) bool { - if r.client == nil { - dg.AddError( - "Unconfigured Client", - "Expected configured client. Please report this issue to the provider developers.", - ) - - return false - } - return true -} - func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { client, diags := clients.ConvertProviderData(request.ProviderData) response.Diagnostics.Append(diags...) diff --git a/internal/kibana/data_view/update.go b/internal/kibana/data_view/update.go index 217c77229..311a4c805 100644 --- a/internal/kibana/data_view/update.go +++ b/internal/kibana/data_view/update.go @@ -8,10 +8,6 @@ import ( ) func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - if !r.resourceReady(&response.Diagnostics) { - return - } - dataviewClient, err := r.client.GetDataViewsClient() if err != nil { response.Diagnostics.AddError("unable to get data view client", err.Error()) diff --git a/internal/kibana/import_saved_objects/create.go b/internal/kibana/import_saved_objects/create.go index 4dad4f434..d429e324d 100644 --- a/internal/kibana/import_saved_objects/create.go +++ b/internal/kibana/import_saved_objects/create.go @@ -19,10 +19,6 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r } func (r *Resource) importObjects(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State, diags *diag.Diagnostics) { - if !resourceReady(r, diags) { - return - } - var model modelV0 diags.Append(plan.Get(ctx, &model)...) diff --git a/internal/kibana/import_saved_objects/schema.go b/internal/kibana/import_saved_objects/schema.go index 1d2b68706..7f448e3a3 100644 --- a/internal/kibana/import_saved_objects/schema.go +++ b/internal/kibana/import_saved_objects/schema.go @@ -5,7 +5,6 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -124,18 +123,6 @@ type Resource struct { client *clients.ApiClient } -func resourceReady(r *Resource, dg *diag.Diagnostics) bool { - if r.client == nil { - dg.AddError( - "Unconfigured Client", - "Expected configured client. Please report this issue to the provider developers.", - ) - - return false - } - return true -} - func (r *Resource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { client, diags := clients.ConvertProviderData(request.ProviderData) response.Diagnostics.Append(diags...) diff --git a/internal/utils/tfsdk.go b/internal/utils/tfsdk.go new file mode 100644 index 000000000..113002949 --- /dev/null +++ b/internal/utils/tfsdk.go @@ -0,0 +1,38 @@ +package utils + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// 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, path path.Path, diags diag.Diagnostics, iteratee func(item T1) T2) types.List { + if value == nil { + return types.ListNull(elemType) + } + + elems := TransformSlice(value, iteratee) + list, nd := types.ListValueFrom(ctx, elemType, elems) + diags.Append(ConvertToAttrDiags(nd, path)...) + + return list +} + +// TransformSlice converts []T1 to []T2 via the iteratee. +func TransformSlice[T1 any, T2 any](value []T1, iteratee func(item T1) T2) []T2 { + if value == nil { + return nil + } + + elems := make([]T2, len(value)) + for i, v := range value { + elems[i] = iteratee(v) + } + + return elems +} diff --git a/internal/utils/tfsdk_test.go b/internal/utils/tfsdk_test.go new file mode 100644 index 000000000..2505b2419 --- /dev/null +++ b/internal/utils/tfsdk_test.go @@ -0,0 +1,101 @@ +package utils_test + +import ( + "context" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSliceToListType(t *testing.T) { + t.Parallel() + + type Type1 struct { + ID string `json:"id"` + } + type Type2 struct { + ID types.String `tfsdk:"id"` + } + elemType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + }, + } + t1_t2 := func(item any) any { + i := item.(Type1) + return Type2{ + ID: types.StringValue(i.ID), + } + } + toString := func(item any) any { + return types.StringValue(item.(string)) + } + + tests := []struct { + name string + input []any + want types.List + elemType attr.Type + iter func(any) any + }{ + { + name: "converts nil", + input: nil, + want: types.ListNull(elemType), + elemType: elemType, + iter: t1_t2, + }, + { + name: "converts empty", + input: []any{}, + want: types.ListValueMust(elemType, []attr.Value{}), + elemType: elemType, + iter: t1_t2, + }, + { + name: "converts struct", + input: []any{ + Type1{ID: "id1"}, + Type1{ID: "id2"}, + Type1{ID: "id3"}, + }, + want: types.ListValueMust(elemType, []attr.Value{ + types.ObjectValueMust(elemType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id1")}), + types.ObjectValueMust(elemType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id2")}), + types.ObjectValueMust(elemType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id3")}), + }), + elemType: elemType, + iter: t1_t2, + }, + { + name: "convert strings", + input: []any{"val1", "val2", "val3"}, + want: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("val1"), + types.StringValue("val2"), + types.StringValue("val3"), + }), + elemType: types.StringType, + iter: toString, + }, + } + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var diags diag.Diagnostics + got := utils.SliceToListType(ctx, tt.input, tt.elemType, path.Empty(), diags, tt.iter) + if !got.Equal(tt.want) { + t.Errorf("SliceToListType() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("SlicetoListType() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) + } +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 9209ecd19..fb802190d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -13,7 +13,9 @@ import ( "github.com/elastic/go-elasticsearch/v7/esapi" providerSchema "github.com/elastic/terraform-provider-elasticstack/internal/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" fwdiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-log/tflog" sdkdiag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -244,3 +246,27 @@ func FlipMap[K comparable, V comparable](m map[K]V) map[V]K { } return inv } + +func SdkDiagsAsError(diags sdkdiag.Diagnostics) error { + for _, diag := range diags { + if diag.Severity == sdkdiag.Error { + return fmt.Errorf("%s: %s", diag.Summary, diag.Detail) + } + } + return nil +} + +// ConvertToAttrDiags wraps an existing collection of diagnostics with an attribute path. +func ConvertToAttrDiags(diags fwdiag.Diagnostics, path path.Path) fwdiag.Diagnostics { + var nd fwdiag.Diagnostics + for _, d := range diags { + if d.Severity() == fwdiag.SeverityError { + nd.AddAttributeError(path, d.Summary(), d.Detail()) + } else if d.Severity() == diag.SeverityWarning { + nd.AddAttributeWarning(path, d.Summary(), d.Detail()) + } else { + nd.Append(d) + } + } + return nd +} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 6f34f912d..57d18b011 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients/config" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/index" "github.com/elastic/terraform-provider-elasticstack/internal/elasticsearch/index/indices" + "github.com/elastic/terraform-provider-elasticstack/internal/fleet/enrollment_tokens" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/data_view" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/import_saved_objects" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/spaces" @@ -72,6 +73,7 @@ func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSour return []func() datasource.DataSource{ indices.NewDataSource, spaces.NewDataSource, + enrollment_tokens.NewDataSource, } } diff --git a/provider/provider.go b/provider/provider.go index f2676b481..c3562774e 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -82,8 +82,7 @@ func New(version string) *schema.Provider { "elasticstack_kibana_action_connector": kibana.DataSourceConnector(), "elasticstack_kibana_security_role": kibana.DataSourceRole(), - "elasticstack_fleet_enrollment_tokens": fleet.DataSourceEnrollmentTokens(), - "elasticstack_fleet_integration": fleet.DataSourceIntegration(), + "elasticstack_fleet_integration": fleet.DataSourceIntegration(), }, ResourcesMap: map[string]*schema.Resource{ "elasticstack_elasticsearch_cluster_settings": cluster.ResourceSettings(),