diff --git a/CHANGELOG.md b/CHANGELOG.md index e29995fd1..7d87c578d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Fix handling of `sys_monitoring` in `elasticstack_fleet_agent_policy` ([#792](https://github.com/elastic/terraform-provider-elasticstack/pull/792)) +- Migrate `elasticstack_fleet_agent_policy`, `elasticstack_fleet_integration` (both), and `elasticstack_fleet_server_host` to terraform-plugin-framework ([#785](https://github.com/elastic/terraform-provider-elasticstack/pull/785)) ## [0.11.7] - 2024-09-20 diff --git a/internal/clients/fleet/fleet.go b/internal/clients/fleet/fleet.go index 0fb963817..778e326f6 100644 --- a/internal/clients/fleet/fleet.go +++ b/internal/clients/fleet/fleet.go @@ -49,10 +49,10 @@ func GetEnrollmentTokensByPolicy(ctx context.Context, client *Client, policyID s } // ReadAgentPolicy reads a specific agent policy from the API. -func ReadAgentPolicy(ctx context.Context, client *Client, id string) (*fleetapi.AgentPolicy, diag.Diagnostics) { +func ReadAgentPolicy(ctx context.Context, client *Client, id string) (*fleetapi.AgentPolicy, fwdiag.Diagnostics) { resp, err := client.API.AgentPolicyInfoWithResponse(ctx, id) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { @@ -61,12 +61,12 @@ func ReadAgentPolicy(ctx context.Context, client *Client, id string) (*fleetapi. case http.StatusNotFound: return nil, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // CreateAgentPolicy creates a new agent policy. -func CreateAgentPolicy(ctx context.Context, client *Client, req fleetapi.AgentPolicyCreateRequest, sysMonitoring bool) (*fleetapi.AgentPolicy, diag.Diagnostics) { +func CreateAgentPolicy(ctx context.Context, client *Client, req fleetapi.AgentPolicyCreateRequest, sysMonitoring bool) (*fleetapi.AgentPolicy, fwdiag.Diagnostics) { resp, err := client.API.CreateAgentPolicyWithResponse(ctx, req, func(ctx context.Context, req *http.Request) error { if sysMonitoring { qs := req.URL.Query() @@ -77,41 +77,41 @@ func CreateAgentPolicy(ctx context.Context, client *Client, req fleetapi.AgentPo return nil }) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { case http.StatusOK: return resp.JSON200.Item, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // UpdateAgentPolicy updates an existing agent policy. -func UpdateAgentPolicy(ctx context.Context, client *Client, id string, req fleetapi.AgentPolicyUpdateRequest) (*fleetapi.AgentPolicy, diag.Diagnostics) { +func UpdateAgentPolicy(ctx context.Context, client *Client, id string, req fleetapi.AgentPolicyUpdateRequest) (*fleetapi.AgentPolicy, fwdiag.Diagnostics) { resp, err := client.API.UpdateAgentPolicyWithResponse(ctx, id, req) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { case http.StatusOK: return &resp.JSON200.Item, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // DeleteAgentPolicy deletes an existing agent policy -func DeleteAgentPolicy(ctx context.Context, client *Client, id string) diag.Diagnostics { +func DeleteAgentPolicy(ctx context.Context, client *Client, id string) fwdiag.Diagnostics { body := fleetapi.DeleteAgentPolicyJSONRequestBody{ AgentPolicyId: id, } resp, err := client.API.DeleteAgentPolicyWithResponse(ctx, body) if err != nil { - return diag.FromErr(err) + return fromErr(err) } switch resp.StatusCode() { @@ -120,7 +120,7 @@ func DeleteAgentPolicy(ctx context.Context, client *Client, id string) diag.Diag case http.StatusNotFound: return nil default: - return reportUnknownError(resp.StatusCode(), resp.Body) + return reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } @@ -189,10 +189,10 @@ func DeleteOutput(ctx context.Context, client *Client, id string) diag.Diagnosti } // ReadFleetServerHost reads a specific fleet server host from the API. -func ReadFleetServerHost(ctx context.Context, client *Client, id string) (*fleetapi.FleetServerHost, diag.Diagnostics) { +func ReadFleetServerHost(ctx context.Context, client *Client, id string) (*fleetapi.FleetServerHost, fwdiag.Diagnostics) { resp, err := client.API.GetOneFleetServerHostsWithResponse(ctx, id) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { @@ -201,45 +201,45 @@ func ReadFleetServerHost(ctx context.Context, client *Client, id string) (*fleet case http.StatusNotFound: return nil, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // CreateFleetServerHost creates a new fleet server host. -func CreateFleetServerHost(ctx context.Context, client *Client, req fleetapi.PostFleetServerHostsJSONRequestBody) (*fleetapi.FleetServerHost, diag.Diagnostics) { +func CreateFleetServerHost(ctx context.Context, client *Client, req fleetapi.PostFleetServerHostsJSONRequestBody) (*fleetapi.FleetServerHost, fwdiag.Diagnostics) { resp, err := client.API.PostFleetServerHostsWithResponse(ctx, req) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { case http.StatusOK: return resp.JSON200.Item, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // UpdateFleetServerHost updates an existing fleet server host. -func UpdateFleetServerHost(ctx context.Context, client *Client, id string, req fleetapi.UpdateFleetServerHostsJSONRequestBody) (*fleetapi.FleetServerHost, diag.Diagnostics) { +func UpdateFleetServerHost(ctx context.Context, client *Client, id string, req fleetapi.UpdateFleetServerHostsJSONRequestBody) (*fleetapi.FleetServerHost, fwdiag.Diagnostics) { resp, err := client.API.UpdateFleetServerHostsWithResponse(ctx, id, req) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { case http.StatusOK: return &resp.JSON200.Item, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // DeleteFleetServerHost deletes an existing fleet server host. -func DeleteFleetServerHost(ctx context.Context, client *Client, id string) diag.Diagnostics { +func DeleteFleetServerHost(ctx context.Context, client *Client, id string) fwdiag.Diagnostics { resp, err := client.API.DeleteFleetServerHostsWithResponse(ctx, id) if err != nil { - return diag.FromErr(err) + return fromErr(err) } switch resp.StatusCode() { @@ -248,7 +248,7 @@ func DeleteFleetServerHost(ctx context.Context, client *Client, id string) diag. case http.StatusNotFound: return nil default: - return reportUnknownError(resp.StatusCode(), resp.Body) + return reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } @@ -333,12 +333,12 @@ func DeletePackagePolicy(ctx context.Context, client *Client, id string, force b } // ReadPackage reads a specific package from the API. -func ReadPackage(ctx context.Context, client *Client, name, version string) diag.Diagnostics { +func ReadPackage(ctx context.Context, client *Client, name, version string) fwdiag.Diagnostics { params := fleetapi.GetPackageParams{} resp, err := client.API.GetPackage(ctx, name, version, ¶ms) if err != nil { - return diag.FromErr(err) + return fromErr(err) } defer resp.Body.Close() @@ -346,19 +346,19 @@ func ReadPackage(ctx context.Context, client *Client, name, version string) diag case http.StatusOK: return nil case http.StatusNotFound: - return diag.FromErr(ErrPackageNotFound) + return fromErr(ErrPackageNotFound) default: errData, err := io.ReadAll(resp.Body) if err != nil { - return diag.FromErr(err) + return fromErr(err) } - return reportUnknownError(resp.StatusCode, errData) + return reportUnknownErrorFw(resp.StatusCode, errData) } } // InstallPackage installs a package. -func InstallPackage(ctx context.Context, client *Client, name, version string, force bool) diag.Diagnostics { +func InstallPackage(ctx context.Context, client *Client, name, version string, force bool) fwdiag.Diagnostics { params := fleetapi.InstallPackageParams{} body := fleetapi.InstallPackageJSONRequestBody{ Force: &force, @@ -367,7 +367,7 @@ func InstallPackage(ctx context.Context, client *Client, name, version string, f resp, err := client.API.InstallPackage(ctx, name, version, ¶ms, body) if err != nil { - return diag.FromErr(err) + return fromErr(err) } defer resp.Body.Close() @@ -377,15 +377,15 @@ func InstallPackage(ctx context.Context, client *Client, name, version string, f default: errData, err := io.ReadAll(resp.Body) if err != nil { - return diag.FromErr(err) + return fromErr(err) } - return reportUnknownError(resp.StatusCode, errData) + return reportUnknownErrorFw(resp.StatusCode, errData) } } // Uninstall uninstalls a package. -func Uninstall(ctx context.Context, client *Client, name, version string, force bool) diag.Diagnostics { +func Uninstall(ctx context.Context, client *Client, name, version string, force bool) fwdiag.Diagnostics { params := fleetapi.DeletePackageParams{} body := fleetapi.DeletePackageJSONRequestBody{ Force: &force, @@ -393,7 +393,7 @@ func Uninstall(ctx context.Context, client *Client, name, version string, force resp, err := client.API.DeletePackageWithResponse(ctx, name, version, ¶ms, body) if err != nil { - return diag.FromErr(err) + return fromErr(err) } switch resp.StatusCode() { @@ -402,26 +402,26 @@ func Uninstall(ctx context.Context, client *Client, name, version string, force case http.StatusNotFound: return nil default: - return reportUnknownError(resp.StatusCode(), resp.Body) + return reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } // AllPackages returns information about the latest packages known to Fleet. -func AllPackages(ctx context.Context, client *Client, prerelease bool) ([]fleetapi.SearchResult, diag.Diagnostics) { +func AllPackages(ctx context.Context, client *Client, prerelease bool) ([]fleetapi.SearchResult, fwdiag.Diagnostics) { params := fleetapi.ListAllPackagesParams{ Prerelease: &prerelease, } resp, err := client.API.ListAllPackagesWithResponse(ctx, ¶ms) if err != nil { - return nil, diag.FromErr(err) + return nil, fromErr(err) } switch resp.StatusCode() { case http.StatusOK: return resp.JSON200.Items, nil default: - return nil, reportUnknownError(resp.StatusCode(), resp.Body) + return nil, reportUnknownErrorFw(resp.StatusCode(), resp.Body) } } diff --git a/internal/fleet/agent_policy/create.go b/internal/fleet/agent_policy/create.go new file mode 100644 index 000000000..86e4a9d25 --- /dev/null +++ b/internal/fleet/agent_policy/create.go @@ -0,0 +1,38 @@ +package agent_policy + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *agentPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var planModel agentPolicyModel + + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + body := planModel.toAPICreateModel() + + sysMonitoring := planModel.SysMonitoring.ValueBool() + policy, diags := fleet.CreateAgentPolicy(ctx, client, body, sysMonitoring) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + planModel.populateFromAPI(policy) + + resp.State.Set(ctx, planModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/agent_policy/delete.go b/internal/fleet/agent_policy/delete.go new file mode 100644 index 000000000..61d54719d --- /dev/null +++ b/internal/fleet/agent_policy/delete.go @@ -0,0 +1,35 @@ +package agent_policy + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *agentPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var stateModel agentPolicyModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + policyID := stateModel.PolicyID.ValueString() + skipDestroy := stateModel.SkipDestroy.ValueBool() + if skipDestroy { + tflog.Debug(ctx, "Skipping destroy of Agent Policy", map[string]any{"policy_id": policyID}) + return + } + + diags = fleet.DeleteAgentPolicy(ctx, client, policyID) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/agent_policy/models.go b/internal/fleet/agent_policy/models.go new file mode 100644 index 000000000..2072b383e --- /dev/null +++ b/internal/fleet/agent_policy/models.go @@ -0,0 +1,104 @@ +package agent_policy + +import ( + "slices" + + fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type agentPolicyModel struct { + ID types.String `tfsdk:"id"` + PolicyID types.String `tfsdk:"policy_id"` + Name types.String `tfsdk:"name"` + Namespace types.String `tfsdk:"namespace"` + Description types.String `tfsdk:"description"` + DataOutputId types.String `tfsdk:"data_output_id"` + MonitoringOutputId types.String `tfsdk:"monitoring_output_id"` + FleetServerHostId types.String `tfsdk:"fleet_server_host_id"` + DownloadSourceId types.String `tfsdk:"download_source_id"` + MonitorLogs types.Bool `tfsdk:"monitor_logs"` + MonitorMetrics types.Bool `tfsdk:"monitor_metrics"` + SysMonitoring types.Bool `tfsdk:"sys_monitoring"` + SkipDestroy types.Bool `tfsdk:"skip_destroy"` +} + +func (model *agentPolicyModel) populateFromAPI(data *fleetapi.AgentPolicy) { + if data == nil { + return + } + + model.ID = types.StringValue(data.Id) + model.PolicyID = types.StringValue(data.Id) + model.DataOutputId = types.StringPointerValue(data.DataOutputId) + model.Description = types.StringPointerValue(data.Description) + model.DownloadSourceId = types.StringPointerValue(data.DownloadSourceId) + model.FleetServerHostId = types.StringPointerValue(data.FleetServerHostId) + + if data.MonitoringEnabled != nil { + if slices.Contains(*data.MonitoringEnabled, fleetapi.AgentPolicyMonitoringEnabledLogs) { + model.MonitorLogs = types.BoolValue(true) + } + if slices.Contains(*data.MonitoringEnabled, fleetapi.AgentPolicyMonitoringEnabledMetrics) { + model.MonitorMetrics = types.BoolValue(true) + } + } + if !utils.IsKnown(model.MonitorLogs) { + model.MonitorLogs = types.BoolValue(false) + } + if !utils.IsKnown(model.MonitorLogs) { + model.MonitorLogs = types.BoolValue(false) + } + + model.MonitoringOutputId = types.StringPointerValue(data.MonitoringOutputId) + model.Name = types.StringValue(data.Name) + model.Namespace = types.StringValue(data.Namespace) +} + +func (model agentPolicyModel) toAPICreateModel() fleetapi.AgentPolicyCreateRequest { + monitoring := make([]fleetapi.AgentPolicyCreateRequestMonitoringEnabled, 0, 2) + if model.MonitorLogs.ValueBool() { + monitoring = append(monitoring, fleetapi.AgentPolicyCreateRequestMonitoringEnabledLogs) + } + if model.MonitorMetrics.ValueBool() { + monitoring = append(monitoring, fleetapi.AgentPolicyCreateRequestMonitoringEnabledMetrics) + } + + body := fleetapi.AgentPolicyCreateRequest{ + DataOutputId: model.DataOutputId.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + DownloadSourceId: model.DownloadSourceId.ValueStringPointer(), + FleetServerHostId: model.FleetServerHostId.ValueStringPointer(), + Id: model.PolicyID.ValueStringPointer(), + MonitoringEnabled: &monitoring, + MonitoringOutputId: model.MonitoringOutputId.ValueStringPointer(), + Name: model.Name.ValueString(), + Namespace: model.Namespace.ValueString(), + } + + return body +} + +func (model agentPolicyModel) toAPIUpdateModel() fleetapi.AgentPolicyUpdateRequest { + monitoring := make([]fleetapi.AgentPolicyUpdateRequestMonitoringEnabled, 0, 2) + if model.MonitorLogs.ValueBool() { + monitoring = append(monitoring, fleetapi.Logs) + } + if model.MonitorMetrics.ValueBool() { + monitoring = append(monitoring, fleetapi.Metrics) + } + + body := fleetapi.AgentPolicyUpdateRequest{ + DataOutputId: model.DataOutputId.ValueStringPointer(), + Description: model.Description.ValueStringPointer(), + DownloadSourceId: model.DownloadSourceId.ValueStringPointer(), + FleetServerHostId: model.FleetServerHostId.ValueStringPointer(), + MonitoringEnabled: &monitoring, + MonitoringOutputId: model.MonitoringOutputId.ValueStringPointer(), + Name: model.Name.ValueString(), + Namespace: model.Namespace.ValueString(), + } + + return body +} diff --git a/internal/fleet/agent_policy/read.go b/internal/fleet/agent_policy/read.go new file mode 100644 index 000000000..43a2c2572 --- /dev/null +++ b/internal/fleet/agent_policy/read.go @@ -0,0 +1,41 @@ +package agent_policy + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *agentPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var stateModel agentPolicyModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + policyID := stateModel.PolicyID.ValueString() + policy, diags := fleet.ReadAgentPolicy(ctx, client, policyID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if policy == nil { + resp.State.RemoveResource(ctx) + return + } + + stateModel.populateFromAPI(policy) + + resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/agent_policy/resource.go b/internal/fleet/agent_policy/resource.go new file mode 100644 index 000000000..5d0628d33 --- /dev/null +++ b/internal/fleet/agent_policy/resource.go @@ -0,0 +1,39 @@ +package agent_policy + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &agentPolicyResource{} + _ resource.ResourceWithConfigure = &agentPolicyResource{} + _ resource.ResourceWithImportState = &agentPolicyResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &agentPolicyResource{} +} + +type agentPolicyResource struct { + client *clients.ApiClient +} + +func (r *agentPolicyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *agentPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "fleet_agent_policy") +} + +func (r *agentPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("policy_id"), req, resp) +} diff --git a/internal/fleet/agent_policy_resource_test.go b/internal/fleet/agent_policy/resource_test.go similarity index 64% rename from internal/fleet/agent_policy_resource_test.go rename to internal/fleet/agent_policy/resource_test.go index 42995d60b..3f8ab7c0d 100644 --- a/internal/fleet/agent_policy_resource_test.go +++ b/internal/fleet/agent_policy/resource_test.go @@ -1,4 +1,4 @@ -package fleet_test +package agent_policy_test import ( "context" @@ -9,6 +9,7 @@ import ( "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" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -18,6 +19,48 @@ import ( var minVersionAgentPolicy = version.Must(version.NewVersion("8.6.0")) +func TestAccResourceAgentPolicyFromSDK(t *testing.T) { + policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceAgentPolicyDestroy, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.7", + }, + }, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionAgentPolicy), + Config: testAccResourceAgentPolicyCreate(policyName, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_logs", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_metrics", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "skip_destroy", "false"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionAgentPolicy), + Config: testAccResourceAgentPolicyCreate(policyName, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "name", fmt.Sprintf("Policy %s", policyName)), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "namespace", "default"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "description", "Test Agent Policy"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_logs", "true"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "monitor_metrics", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "skip_destroy", "false"), + ), + }, + }, + }) +} + func TestAccResourceAgentPolicy(t *testing.T) { policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) @@ -50,6 +93,14 @@ func TestAccResourceAgentPolicy(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_fleet_agent_policy.test_policy", "skip_destroy", "false"), ), }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionAgentPolicy), + Config: testAccResourceAgentPolicyUpdate(policyName, false), + ResourceName: "elasticstack_fleet_agent_policy.test_policy", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"skip_destroy"}, + }, }, }) } @@ -138,11 +189,11 @@ func checkResourceAgentPolicyDestroy(s *terraform.State) error { if err != nil { return err } - packagePolicy, diag := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) - if diag.HasError() { - return errors.New(diag[0].Summary) + policy, diags := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) + if diags.HasError() { + return utils.FwDiagsAsError(diags) } - if packagePolicy != nil { + if policy != nil { return fmt.Errorf("agent policy id=%v still exists, but it should have been removed", rs.Primary.ID) } } @@ -164,16 +215,16 @@ func checkResourceAgentPolicySkipDestroy(s *terraform.State) error { if err != nil { return err } - packagePolicy, diag := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) - if diag.HasError() { - return errors.New(diag[0].Summary) + policy, diags := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) + if diags.HasError() { + return utils.FwDiagsAsError(diags) } - if packagePolicy == nil { + if policy == nil { return fmt.Errorf("agent policy id=%v does not exist, but should still exist when skip_destroy is true", rs.Primary.ID) } - if diag = fleet.DeleteAgentPolicy(context.Background(), fleetClient, rs.Primary.ID); diag.HasError() { - return errors.New(diag[0].Summary) + if diags = fleet.DeleteAgentPolicy(context.Background(), fleetClient, rs.Primary.ID); diags.HasError() { + return errors.New(diags.Errors()[0].Summary()) } } return nil diff --git a/internal/fleet/agent_policy/schema.go b/internal/fleet/agent_policy/schema.go new file mode 100644 index 000000000..c64d7319a --- /dev/null +++ b/internal/fleet/agent_policy/schema.go @@ -0,0 +1,81 @@ +package agent_policy + +import ( + "context" + + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +func (r *agentPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema.Description = "Creates a new Fleet Agent Policy. See https://www.elastic.co/guide/en/fleet/current/agent-policy.html" + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "policy_id": schema.StringAttribute{ + Description: "Unique identifier of the agent policy.", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the agent policy.", + Required: true, + }, + "namespace": schema.StringAttribute{ + Description: "The namespace of the agent policy.", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "The description of the agent policy.", + Optional: true, + }, + "data_output_id": schema.StringAttribute{ + Description: "The identifier for the data output.", + Optional: true, + }, + "monitoring_output_id": schema.StringAttribute{ + Description: "The identifier for monitoring output.", + Optional: true, + }, + "fleet_server_host_id": schema.StringAttribute{ + Description: "The identifier for the Fleet server host.", + Optional: true, + }, + "download_source_id": schema.StringAttribute{ + Description: "The identifier for the Elastic Agent binary download server.", + Optional: true, + }, + "monitor_logs": schema.BoolAttribute{ + Description: "Enable collection of agent logs.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "monitor_metrics": schema.BoolAttribute{ + Description: "Enable collection of agent metrics.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(false), + }, + "skip_destroy": schema.BoolAttribute{ + Description: "Set to true if you do not wish the agent policy to be deleted at destroy time, and instead just remove the agent policy from the Terraform state.", + Optional: true, + }, + "sys_monitoring": schema.BoolAttribute{ + Description: "Enable collection of system logs and metrics.", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + } +} diff --git a/internal/fleet/agent_policy/update.go b/internal/fleet/agent_policy/update.go new file mode 100644 index 000000000..68313eeee --- /dev/null +++ b/internal/fleet/agent_policy/update.go @@ -0,0 +1,38 @@ +package agent_policy + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *agentPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var planModel agentPolicyModel + + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + body := planModel.toAPIUpdateModel() + + policyID := planModel.PolicyID.ValueString() + policy, diags := fleet.UpdateAgentPolicy(ctx, client, policyID, body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + planModel.populateFromAPI(policy) + + diags = resp.State.Set(ctx, planModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/agent_policy_resource.go b/internal/fleet/agent_policy_resource.go deleted file mode 100644 index 6eb5ae338..000000000 --- a/internal/fleet/agent_policy_resource.go +++ /dev/null @@ -1,289 +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/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -const ( - monitorLogs = "logs" - monitorMetrics = "metrics" -) - -func ResourceAgentPolicy() *schema.Resource { - agentPolicySchema := map[string]*schema.Schema{ - "policy_id": { - Description: "Unique identifier of the agent policy.", - Type: schema.TypeString, - Computed: true, - Optional: true, - ForceNew: true, - }, - "name": { - Description: "The name of the agent policy.", - Type: schema.TypeString, - Required: true, - }, - "namespace": { - Description: "The namespace of the agent policy.", - Type: schema.TypeString, - Required: true, - }, - "description": { - Description: "The description of the agent policy.", - Type: schema.TypeString, - Optional: true, - }, - "data_output_id": { - Description: "The identifier for the data output.", - Type: schema.TypeString, - Optional: true, - }, - "monitoring_output_id": { - Description: "The identifier for monitoring output.", - Type: schema.TypeString, - Optional: true, - }, - "fleet_server_host_id": { - Description: "The identifier for the Fleet server host.", - Type: schema.TypeString, - Optional: true, - }, - "download_source_id": { - Description: "The identifier for the Elastic Agent binary download server.", - Type: schema.TypeString, - Optional: true, - }, - "sys_monitoring": { - Description: "Enable collection of system logs and metrics.", - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - }, - "monitor_logs": { - Description: "Enable collection of agent logs.", - Type: schema.TypeBool, - Optional: true, - }, - "monitor_metrics": { - Description: "Enable collection of agent metrics.", - Type: schema.TypeBool, - Optional: true, - }, - "skip_destroy": { - Description: "Set to true if you do not wish the agent policy to be deleted at destroy time, and instead just remove the agent policy from the Terraform state.", - Type: schema.TypeBool, - Optional: true, - }, - } - - return &schema.Resource{ - Description: "Creates a new Fleet Agent Policy. See https://www.elastic.co/guide/en/fleet/current/agent-policy.html", - - CreateContext: resourceAgentPolicyCreate, - ReadContext: resourceAgentPolicyRead, - UpdateContext: resourceAgentPolicyUpdate, - DeleteContext: resourceAgentPolicyDelete, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: agentPolicySchema, - } -} - -func resourceAgentPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - if id := d.Get("policy_id").(string); id != "" { - d.SetId(id) - } - - req := fleetapi.AgentPolicyCreateRequest{ - Name: d.Get("name").(string), - Namespace: d.Get("namespace").(string), - } - - if value := d.Get("policy_id").(string); value != "" { - req.Id = &value - } - if value := d.Get("description").(string); value != "" { - req.Description = &value - } - if value := d.Get("data_output_id").(string); value != "" { - req.DataOutputId = &value - } - if value := d.Get("download_source_id").(string); value != "" { - req.DownloadSourceId = &value - } - if value := d.Get("fleet_server_host_id").(string); value != "" { - req.FleetServerHostId = &value - } - if value := d.Get("monitoring_output_id").(string); value != "" { - req.MonitoringOutputId = &value - } - - monitoringValues := make([]fleetapi.AgentPolicyCreateRequestMonitoringEnabled, 0, 2) - if value := d.Get("monitor_logs").(bool); value { - monitoringValues = append(monitoringValues, monitorLogs) - } - if value := d.Get("monitor_metrics").(bool); value { - monitoringValues = append(monitoringValues, monitorMetrics) - } - req.MonitoringEnabled = &monitoringValues - - sysMonitoring := d.Get("sys_monitoring").(bool) - policy, diags := fleet.CreateAgentPolicy(ctx, fleetClient, req, sysMonitoring) - if diags.HasError() { - return diags - } - - d.SetId(policy.Id) - if err := d.Set("policy_id", policy.Id); err != nil { - return diag.FromErr(err) - } - - return resourceAgentPolicyRead(ctx, d, meta) -} - -func resourceAgentPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - req := fleetapi.AgentPolicyUpdateRequest{ - Name: d.Get("name").(string), - Namespace: d.Get("namespace").(string), - } - - if value := d.Get("description").(string); value != "" { - req.Description = &value - } - if value := d.Get("data_output_id").(string); value != "" { - req.DataOutputId = &value - } - if value := d.Get("download_source_id").(string); value != "" { - req.DownloadSourceId = &value - } - if value := d.Get("fleet_server_host_id").(string); value != "" { - req.FleetServerHostId = &value - } - if value := d.Get("monitoring_output_id").(string); value != "" { - req.MonitoringOutputId = &value - } - - monitoringValues := make([]fleetapi.AgentPolicyUpdateRequestMonitoringEnabled, 0, 2) - if value := d.Get("monitor_logs").(bool); value { - monitoringValues = append(monitoringValues, monitorLogs) - } - if value := d.Get("monitor_metrics").(bool); value { - monitoringValues = append(monitoringValues, monitorMetrics) - } - req.MonitoringEnabled = &monitoringValues - - _, diags = fleet.UpdateAgentPolicy(ctx, fleetClient, d.Id(), req) - if diags.HasError() { - return diags - } - - return resourceAgentPolicyRead(ctx, d, meta) -} - -func resourceAgentPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - agentPolicy, diags := fleet.ReadAgentPolicy(ctx, fleetClient, d.Id()) - if diags.HasError() { - return diags - } - - // Not found. - if agentPolicy == nil { - d.SetId("") - return nil - } - - if err := d.Set("name", agentPolicy.Name); err != nil { - return diag.FromErr(err) - } - if err := d.Set("namespace", agentPolicy.Namespace); err != nil { - return diag.FromErr(err) - } - if err := d.Set("policy_id", agentPolicy.Id); err != nil { - return diag.FromErr(err) - } - if agentPolicy.Description != nil { - if err := d.Set("description", *agentPolicy.Description); err != nil { - return diag.FromErr(err) - } - } - if agentPolicy.DataOutputId != nil { - if err := d.Set("data_output_id", *agentPolicy.DataOutputId); err != nil { - return diag.FromErr(err) - } - } - if agentPolicy.DownloadSourceId != nil { - if err := d.Set("download_source_id", *agentPolicy.DownloadSourceId); err != nil { - return diag.FromErr(err) - } - } - if agentPolicy.FleetServerHostId != nil { - if err := d.Set("fleet_server_host_id", *agentPolicy.FleetServerHostId); err != nil { - return diag.FromErr(err) - } - } - if agentPolicy.MonitoringOutputId != nil { - if err := d.Set("monitoring_output_id", *agentPolicy.MonitoringOutputId); err != nil { - return diag.FromErr(err) - } - } - if agentPolicy.MonitoringEnabled != nil { - for _, v := range *agentPolicy.MonitoringEnabled { - switch v { - case monitorLogs: - if err := d.Set("monitor_logs", true); err != nil { - return diag.FromErr(err) - } - case monitorMetrics: - if err := d.Set("monitor_metrics", true); err != nil { - return diag.FromErr(err) - - } - } - } - } - - return nil -} - -func resourceAgentPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - if d.Get("skip_destroy").(bool) { - tflog.Debug(ctx, "Skipping destroy of Agent Policy", map[string]interface{}{"policy_id": d.Id()}) - return nil - } - - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - if diags = fleet.DeleteAgentPolicy(ctx, fleetClient, d.Id()); diags.HasError() { - return diags - } - d.SetId("") - - return diags -} diff --git a/internal/fleet/enrollment_tokens/data_source_test.go b/internal/fleet/enrollment_tokens/data_source_test.go index bc897c198..586170ec4 100644 --- a/internal/fleet/enrollment_tokens/data_source_test.go +++ b/internal/fleet/enrollment_tokens/data_source_test.go @@ -70,7 +70,7 @@ func checkResourceAgentPolicyDestroy(s *terraform.State) error { } policy, diags := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) if diags.HasError() { - return utils.SdkDiagsAsError(diags) + return utils.FwDiagsAsError(diags) } if policy != nil { return fmt.Errorf("agent policy id=%v still exists, but it should have been removed", rs.Primary.ID) diff --git a/internal/fleet/fleet_server_host_resource.go b/internal/fleet/fleet_server_host_resource.go deleted file mode 100644 index fff7c6bd0..000000000 --- a/internal/fleet/fleet_server_host_resource.go +++ /dev/null @@ -1,174 +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/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func ResourceFleetServerHost() *schema.Resource { - fleetServerHostSchema := map[string]*schema.Schema{ - "host_id": { - Description: "Unique identifier of the Fleet server host.", - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "name": { - Description: "The name of the Fleet server host.", - Type: schema.TypeString, - Required: true, - }, - "hosts": { - Description: "A list of hosts.", - Type: schema.TypeList, - Required: true, - MinItems: 1, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "default": { - Description: "Set as default.", - Type: schema.TypeBool, - Optional: true, - }, - } - - return &schema.Resource{ - Description: "Creates a new Fleet Server Host.", - - CreateContext: resourceFleetServerHostCreate, - ReadContext: resourceFleetServerHostRead, - UpdateContext: resourceFleetServerHostUpdate, - DeleteContext: resourceFleetServerHostDelete, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: fleetServerHostSchema, - } -} - -func resourceFleetServerHostCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - req := fleetapi.PostFleetServerHostsJSONRequestBody{ - Name: d.Get("name").(string), - } - - if id := d.Get("host_id").(string); id != "" { - d.SetId(id) - req.Id = &id - } - - if value := d.Get("hosts").([]interface{}); len(value) > 0 { - for _, v := range value { - if vStr, ok := v.(string); ok && vStr != "" { - req.HostUrls = append(req.HostUrls, vStr) - } - } - } - if value := d.Get("default").(bool); value { - req.IsDefault = &value - } - - host, diags := fleet.CreateFleetServerHost(ctx, fleetClient, req) - if diags.HasError() { - return diags - } - - d.SetId(host.Id) - if err := d.Set("host_id", host.Id); err != nil { - return diag.FromErr(err) - } - - return resourceFleetServerHostRead(ctx, d, meta) -} - -func resourceFleetServerHostUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - req := fleetapi.UpdateFleetServerHostsJSONRequestBody{} - - if value, ok := d.Get("name").(string); ok && value != "" { - req.Name = &value - } - var hosts []string - if value, ok := d.Get("hosts").([]interface{}); ok && len(value) > 0 { - for _, v := range value { - if vStr, ok := v.(string); ok && vStr != "" { - hosts = append(hosts, vStr) - } - } - } - if hosts != nil { - req.HostUrls = &hosts - } - if value := d.Get("default").(bool); value { - req.IsDefault = &value - } - - _, diags = fleet.UpdateFleetServerHost(ctx, fleetClient, d.Id(), req) - if diags.HasError() { - return diags - } - - return resourceFleetServerHostRead(ctx, d, meta) -} - -func resourceFleetServerHostRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - host, diags := fleet.ReadFleetServerHost(ctx, fleetClient, d.Id()) - if diags.HasError() { - return diags - } - - // Not found. - if host == nil { - d.SetId("") - return nil - } - - if host.Name != nil { - if err := d.Set("name", *host.Name); err != nil { - return diag.FromErr(err) - } - } - if err := d.Set("hosts", host.HostUrls); err != nil { - return diag.FromErr(err) - } - if err := d.Set("default", host.IsDefault); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceFleetServerHostDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - if diags = fleet.DeleteFleetServerHost(ctx, fleetClient, d.Id()); diags.HasError() { - return diags - } - d.SetId("") - - return diags -} diff --git a/internal/fleet/integration/create.go b/internal/fleet/integration/create.go new file mode 100644 index 000000000..e166cf45c --- /dev/null +++ b/internal/fleet/integration/create.go @@ -0,0 +1,45 @@ +package integration + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *integrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + 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) { + var planModel integrationModel + + diags := plan.Get(ctx, &planModel) + respDiags.Append(diags...) + if respDiags.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + respDiags.AddError(err.Error(), "") + return + } + + name := planModel.Name.ValueString() + version := planModel.Version.ValueString() + force := planModel.Force.ValueBool() + diags = fleet.InstallPackage(ctx, client, name, version, force) + respDiags.Append(diags...) + if respDiags.HasError() { + return + } + + planModel.ID = types.StringValue(getPackageID(name, version)) + + diags = state.Set(ctx, planModel) + respDiags.Append(diags...) +} diff --git a/internal/fleet/integration/delete.go b/internal/fleet/integration/delete.go new file mode 100644 index 000000000..6a9d4af80 --- /dev/null +++ b/internal/fleet/integration/delete.go @@ -0,0 +1,37 @@ +package integration + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (r *integrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var stateModel integrationModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + name := stateModel.Name.ValueString() + version := stateModel.Version.ValueString() + force := stateModel.Force.ValueBool() + skipDestroy := stateModel.SkipDestroy.ValueBool() + if skipDestroy { + tflog.Debug(ctx, "Skipping uninstall of integration package", map[string]any{"name": name, "version": version}) + return + } + + diags = fleet.Uninstall(ctx, client, name, version, force) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/integration/models.go b/internal/fleet/integration/models.go new file mode 100644 index 000000000..4d1029006 --- /dev/null +++ b/internal/fleet/integration/models.go @@ -0,0 +1,19 @@ +package integration + +import ( + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type integrationModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Version types.String `tfsdk:"version"` + Force types.Bool `tfsdk:"force"` + SkipDestroy types.Bool `tfsdk:"skip_destroy"` +} + +func getPackageID(name string, version string) string { + hash, _ := utils.StringToHash(name + version) + return *hash +} diff --git a/internal/fleet/integration/read.go b/internal/fleet/integration/read.go new file mode 100644 index 000000000..fbd345851 --- /dev/null +++ b/internal/fleet/integration/read.go @@ -0,0 +1,39 @@ +package integration + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *integrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var stateModel integrationModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + name := stateModel.Name.ValueString() + version := stateModel.Version.ValueString() + diags = fleet.ReadPackage(ctx, client, name, version) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.State.RemoveResource(ctx) + return + } + + stateModel.ID = types.StringValue(getPackageID(name, version)) + + diags = resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/integration/resource.go b/internal/fleet/integration/resource.go new file mode 100644 index 000000000..7b2bf4c7a --- /dev/null +++ b/internal/fleet/integration/resource.go @@ -0,0 +1,33 @@ +package integration + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &integrationResource{} + _ resource.ResourceWithConfigure = &integrationResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &integrationResource{} +} + +type integrationResource struct { + client *clients.ApiClient +} + +func (r *integrationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *integrationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "fleet_integration") +} diff --git a/internal/fleet/integration/resource_test.go b/internal/fleet/integration/resource_test.go new file mode 100644 index 000000000..87826e7b8 --- /dev/null +++ b/internal/fleet/integration/resource_test.go @@ -0,0 +1,83 @@ +package integration_test + +import ( + "regexp" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +var minVersionIntegration = version.Must(version.NewVersion("8.6.0")) + +func TestAccResourceIntegrationFromSDK(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.7", + }, + }, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + Config: testAccResourceIntegration, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "name", "tcp"), + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "version", "1.16.0"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + Config: testAccResourceIntegration, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "name", "tcp"), + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "version", "1.16.0"), + ), + }, + }, + }) +} + +func TestAccResourceIntegration(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + Config: testAccResourceIntegration, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "name", "tcp"), + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "version", "1.16.0"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + ResourceName: "elasticstack_fleet_integration.test_integration", + Config: testAccResourceIntegration, + ImportState: true, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("Resource Import Not Implemented"), + }, + }, + }) +} + +const testAccResourceIntegration = ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_integration" "test_integration" { + name = "tcp" + version = "1.16.0" + force = true + skip_destroy = true +} +` diff --git a/internal/fleet/integration/schema.go b/internal/fleet/integration/schema.go new file mode 100644 index 000000000..ef87f4a85 --- /dev/null +++ b/internal/fleet/integration/schema.go @@ -0,0 +1,42 @@ +package integration + +import ( + "context" + + "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" +) + +func (r *integrationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema.Description = "Manage installation of a Fleet integration package." + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "The integration package name.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "version": schema.StringAttribute{ + Description: "The integration package version.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "force": schema.BoolAttribute{ + Description: "Set to true to force the requested action.", + Optional: true, + }, + "skip_destroy": schema.BoolAttribute{ + Description: "Set to true if you do not wish the integration package to be uninstalled at destroy time, and instead just remove the integration package from the Terraform state.", + Optional: true, + }, + } +} diff --git a/internal/fleet/integration/update.go b/internal/fleet/integration/update.go new file mode 100644 index 000000000..7e4bc9405 --- /dev/null +++ b/internal/fleet/integration/update.go @@ -0,0 +1,11 @@ +package integration + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *integrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.create(ctx, req.Plan, &resp.State, resp.Diagnostics) +} diff --git a/internal/fleet/integration_data_source.go b/internal/fleet/integration_data_source.go deleted file mode 100644 index 0844df024..000000000 --- a/internal/fleet/integration_data_source.go +++ /dev/null @@ -1,74 +0,0 @@ -package fleet - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" -) - -func DataSourceIntegration() *schema.Resource { - packageSchema := map[string]*schema.Schema{ - "name": { - Description: "The integration package name.", - Type: schema.TypeString, - Required: true, - }, - "prerelease": { - Description: "Include prerelease packages.", - Type: schema.TypeBool, - Optional: true, - }, - "version": { - Description: "The integration package version.", - Type: schema.TypeString, - Computed: true, - }, - } - - return &schema.Resource{ - Description: "Retrieves the latest version of an integration package in Fleet.", - - ReadContext: dataSourceIntegrationRead, - - Schema: packageSchema, - } -} - -func dataSourceIntegrationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - pkgName := d.Get("name").(string) - if d.Id() == "" { - hash, err := utils.StringToHash(pkgName) - if err != nil { - return diag.FromErr(err) - } - d.SetId(*hash) - } - - prerelease := d.Get("prerelease").(bool) - allPackages, diags := fleet.AllPackages(ctx, fleetClient, prerelease) - if diags.HasError() { - return diags - } - - for _, v := range allPackages { - if v.Name != pkgName { - continue - } - - if err := d.Set("version", v.Version); err != nil { - return diag.FromErr(err) - } - break - } - - return diags -} diff --git a/internal/fleet/integration_ds/data_source.go b/internal/fleet/integration_ds/data_source.go new file mode 100644 index 000000000..9d81038c4 --- /dev/null +++ b/internal/fleet/integration_ds/data_source.go @@ -0,0 +1,33 @@ +package integration_ds + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/datasource" +) + +var ( + _ datasource.DataSource = &integrationDataSource{} + _ datasource.DataSourceWithConfigure = &integrationDataSource{} +) + +// NewDataSource is a helper function to simplify the provider implementation. +func NewDataSource() datasource.DataSource { + return &integrationDataSource{} +} + +type integrationDataSource struct { + client *clients.ApiClient +} + +func (d *integrationDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + d.client = client +} + +func (d *integrationDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "fleet_integration") +} diff --git a/internal/fleet/integration_data_source_test.go b/internal/fleet/integration_ds/data_source_test.go similarity index 97% rename from internal/fleet/integration_data_source_test.go rename to internal/fleet/integration_ds/data_source_test.go index f059cd627..ab5d50734 100644 --- a/internal/fleet/integration_data_source_test.go +++ b/internal/fleet/integration_ds/data_source_test.go @@ -1,15 +1,14 @@ -package fleet_test +package integration_ds_test import ( "fmt" "testing" + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "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" - - "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" ) var minVersionIntegrationDataSource = version.Must(version.NewVersion("8.6.0")) @@ -38,7 +37,7 @@ provider "elasticstack" { } data "elasticstack_fleet_integration" "test" { - name = "tcp" + name = "tcp" } ` diff --git a/internal/fleet/integration_ds/models.go b/internal/fleet/integration_ds/models.go new file mode 100644 index 000000000..f489852e1 --- /dev/null +++ b/internal/fleet/integration_ds/models.go @@ -0,0 +1,23 @@ +package integration_ds + +import ( + fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type integrationDataSourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Prerelease types.Bool `tfsdk:"prerelease"` + Version types.String `tfsdk:"version"` +} + +func (m *integrationDataSourceModel) populateFromAPI(pkgName string, packages []fleetapi.SearchResult) { + m.Version = types.StringNull() + for _, pkg := range packages { + if pkg.Name == pkgName { + m.Version = types.StringValue(pkg.Version) + return + } + } +} diff --git a/internal/fleet/integration_ds/read.go b/internal/fleet/integration_ds/read.go new file mode 100644 index 000000000..7c0acf45b --- /dev/null +++ b/internal/fleet/integration_ds/read.go @@ -0,0 +1,48 @@ +package integration_ds + +import ( + "context" + + "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 *integrationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model integrationDataSourceModel + + 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 + } + + name := model.Name.ValueString() + prerelease := model.Prerelease.ValueBool() + packages, diags := fleet.AllPackages(ctx, client, prerelease) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if model.ID.ValueString() == "" { + hash, err := utils.StringToHash(name) + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + model.ID = types.StringPointerValue(hash) + } + + model.populateFromAPI(name, packages) + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/integration_ds/schema.go b/internal/fleet/integration_ds/schema.go new file mode 100644 index 000000000..8ab07a904 --- /dev/null +++ b/internal/fleet/integration_ds/schema.go @@ -0,0 +1,30 @@ +package integration_ds + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func (d *integrationDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema.Description = "Retrieves the latest version of an integration package in Fleet." + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "The integration package name.", + Required: true, + }, + "prerelease": schema.BoolAttribute{ + Description: "Include prerelease packages.", + Optional: true, + }, + "version": schema.StringAttribute{ + Description: "The integration package version.", + Computed: true, + }, + } +} diff --git a/internal/fleet/integration_policy_resource_test.go b/internal/fleet/integration_policy_resource_test.go index 00e06ad5a..beb9fe7f8 100644 --- a/internal/fleet/integration_policy_resource_test.go +++ b/internal/fleet/integration_policy_resource_test.go @@ -14,6 +14,7 @@ import ( "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" ) @@ -81,7 +82,7 @@ func checkResourceIntegrationPolicyDestroy(s *terraform.State) error { case "elasticstack_fleet_agent_policy": agentPolicy, diag := fleet.ReadAgentPolicy(context.Background(), fleetClient, rs.Primary.ID) if diag.HasError() { - return errors.New(diag[0].Summary) + return utils.FwDiagsAsError(diag) } if agentPolicy != nil { return fmt.Errorf("agent policy id=%v still exists, but it should have been removed", rs.Primary.ID) diff --git a/internal/fleet/integration_resource.go b/internal/fleet/integration_resource.go deleted file mode 100644 index 71013064e..000000000 --- a/internal/fleet/integration_resource.go +++ /dev/null @@ -1,122 +0,0 @@ -package fleet - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" -) - -func getPackageID(name, version string) string { - hash, _ := utils.StringToHash(name + version) - - return *hash -} - -func ResourceIntegration() *schema.Resource { - packageSchema := map[string]*schema.Schema{ - "name": { - Description: "The integration package name.", - Type: schema.TypeString, - ForceNew: true, - Required: true, - }, - "version": { - Description: "The integration package version.", - Type: schema.TypeString, - ForceNew: true, - Required: true, - }, - "force": { - Description: "Set to true to force the requested action.", - Type: schema.TypeBool, - Optional: true, - }, - "skip_destroy": { - Description: "Set to true if you do not wish the integration package to be uninstalled at destroy time, and instead just remove the integration package from the Terraform state.", - Type: schema.TypeBool, - Optional: true, - }, - } - - return &schema.Resource{ - Description: "Manage installation of a Fleet integration package.", - - CreateContext: resourceIntegrationInstall, - ReadContext: resourceIntegrationRead, - UpdateContext: resourceIntegrationInstall, - DeleteContext: resourceIntegrationDelete, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: packageSchema, - } -} - -func resourceIntegrationInstall(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - name := d.Get("name").(string) - version := d.Get("version").(string) - force := d.Get("force").(bool) - - d.SetId(getPackageID(name, version)) - - if diags = fleet.InstallPackage(ctx, fleetClient, name, version, force); diags.HasError() { - return diags - } - - return resourceIntegrationRead(ctx, d, meta) -} - -func resourceIntegrationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - name := d.Get("name").(string) - version := d.Get("version").(string) - - d.SetId(getPackageID(name, version)) - - if diags = fleet.ReadPackage(ctx, fleetClient, name, version); diags.HasError() { - return diags - } - - return nil -} - -func resourceIntegrationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - name := d.Get("name").(string) - version := d.Get("version").(string) - force := d.Get("force").(bool) - - if d.Get("skip_destroy").(bool) { - tflog.Debug(ctx, "Skipping uninstall of integration package", map[string]interface{}{"name": name, "version": version}) - return nil - } - - d.SetId(getPackageID(name, version)) - - fleetClient, diags := getFleetClient(d, meta) - if diags.HasError() { - return diags - } - - if diags = fleet.Uninstall(ctx, fleetClient, name, version, force); diags.HasError() { - return diags - } - d.SetId("") - - return diags -} diff --git a/internal/fleet/integration_resource_test.go b/internal/fleet/integration_resource_test.go deleted file mode 100644 index adec1ac3c..000000000 --- a/internal/fleet/integration_resource_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package fleet_test - -import ( - "testing" - - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - - "github.com/elastic/terraform-provider-elasticstack/internal/acctest" - "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" -) - -var minVersionIntegration = version.Must(version.NewVersion("8.6.0")) - -const integrationConfig = ` -provider "elasticstack" { - elasticsearch {} - kibana {} -} - -resource "elasticstack_fleet_integration" "test_integration" { - name = "tcp" - version = "1.16.0" - force = true - skip_destroy = true -} -` - -func TestAccResourceIntegration(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.Providers, - Steps: []resource.TestStep{ - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), - Config: integrationConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "name", "tcp"), - resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "version", "1.16.0"), - ), - }, - }, - }) -} diff --git a/internal/fleet/server_host/create.go b/internal/fleet/server_host/create.go new file mode 100644 index 000000000..7cb055c5d --- /dev/null +++ b/internal/fleet/server_host/create.go @@ -0,0 +1,45 @@ +package server_host + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *serverHostResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var planModel serverHostModel + + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + body, diags := planModel.toAPICreateModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + host, diags := fleet.CreateFleetServerHost(ctx, client, body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = planModel.populateFromAPI(ctx, host) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, planModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/server_host/delete.go b/internal/fleet/server_host/delete.go new file mode 100644 index 000000000..90c5c87b5 --- /dev/null +++ b/internal/fleet/server_host/delete.go @@ -0,0 +1,28 @@ +package server_host + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *serverHostResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var stateModel serverHostModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + hostID := stateModel.HostID.ValueString() + diags = fleet.DeleteFleetServerHost(ctx, client, hostID) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/server_host/models.go b/internal/fleet/server_host/models.go new file mode 100644 index 000000000..6817ad6f4 --- /dev/null +++ b/internal/fleet/server_host/models.go @@ -0,0 +1,52 @@ +package server_host + +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 serverHostModel struct { + Id types.String `tfsdk:"id"` + HostID types.String `tfsdk:"host_id"` + Name types.String `tfsdk:"name"` + Hosts types.List `tfsdk:"hosts"` + Default types.Bool `tfsdk:"default"` +} + +func (model *serverHostModel) populateFromAPI(ctx context.Context, data *fleetapi.FleetServerHost) (diags diag.Diagnostics) { + if data == nil { + return nil + } + + 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.Default = types.BoolValue(data.IsDefault) + + return +} + +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), + Id: model.HostID.ValueStringPointer(), + IsDefault: model.Default.ValueBoolPointer(), + Name: model.Name.ValueString(), + } + return +} + +func (model serverHostModel) toAPIUpdateModel(ctx context.Context) (body fleetapi.UpdateFleetServerHostsJSONRequestBody, diags diag.Diagnostics) { + body = fleetapi.UpdateFleetServerHostsJSONRequestBody{ + HostUrls: utils.Pointer(utils.ListTypeToSlice_String(ctx, model.Hosts, path.Root("hosts"), diags)), + IsDefault: model.Default.ValueBoolPointer(), + Name: model.Name.ValueStringPointer(), + } + return +} diff --git a/internal/fleet/server_host/read.go b/internal/fleet/server_host/read.go new file mode 100644 index 000000000..138249b2f --- /dev/null +++ b/internal/fleet/server_host/read.go @@ -0,0 +1,45 @@ +package server_host + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *serverHostResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var stateModel serverHostModel + + diags := req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + hostID := stateModel.HostID.ValueString() + host, diags := fleet.ReadFleetServerHost(ctx, client, hostID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if host == nil { + resp.State.RemoveResource(ctx) + return + } + + diags = stateModel.populateFromAPI(ctx, host) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, stateModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/fleet/server_host/resource.go b/internal/fleet/server_host/resource.go new file mode 100644 index 000000000..37332aaef --- /dev/null +++ b/internal/fleet/server_host/resource.go @@ -0,0 +1,39 @@ +package server_host + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &serverHostResource{} + _ resource.ResourceWithConfigure = &serverHostResource{} + _ resource.ResourceWithImportState = &serverHostResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &serverHostResource{} +} + +type serverHostResource struct { + client *clients.ApiClient +} + +func (r *serverHostResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *serverHostResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "fleet_server_host") +} + +func (r *serverHostResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("host_id"), req, resp) +} diff --git a/internal/fleet/fleet_server_host_resource_test.go b/internal/fleet/server_host/resource_test.go similarity index 57% rename from internal/fleet/fleet_server_host_resource_test.go rename to internal/fleet/server_host/resource_test.go index 3717e3186..a9f514b5f 100644 --- a/internal/fleet/fleet_server_host_resource_test.go +++ b/internal/fleet/server_host/resource_test.go @@ -1,14 +1,14 @@ -package fleet_test +package server_host_test import ( "context" - "errors" "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" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -18,8 +18,47 @@ import ( var minVersionFleetServerHost = version.Must(version.NewVersion("8.6.0")) +func TestAccResourceFleetServerHostFromSDK(t *testing.T) { + policyName := sdkacctest.RandString(22) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceFleetServerHostDestroy, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "elasticstack": { + Source: "elastic/elasticstack", + VersionConstraint: "0.11.7", + }, + }, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionFleetServerHost), + Config: testAccResourceFleetServerHostCreate(policyName), + + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "name", fmt.Sprintf("FleetServerHost %s", policyName)), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "id", "fleet-server-host-id"), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "default", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "hosts.0", "https://fleet-server:8220"), + ), + }, + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionFleetServerHost), + Config: testAccResourceFleetServerHostCreate(policyName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "name", fmt.Sprintf("FleetServerHost %s", policyName)), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "id", "fleet-server-host-id"), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "default", "false"), + resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "hosts.0", "https://fleet-server:8220"), + ), + }, + }, + }) +} + func TestAccResourceFleetServerHost(t *testing.T) { - policyName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + policyName := sdkacctest.RandString(22) resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -46,6 +85,13 @@ func TestAccResourceFleetServerHost(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_fleet_server_host.test_host", "hosts.0", "https://fleet-server:8220"), ), }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionFleetServerHost), + Config: testAccResourceFleetServerHostUpdate(policyName), + ResourceName: "elasticstack_fleet_server_host.test_host", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -83,7 +129,6 @@ resource "elasticstack_fleet_server_host" "test_host" { "https://fleet-server:8220" ] } - `, fmt.Sprintf("Updated FleetServerHost %s", id)) } @@ -102,12 +147,12 @@ func checkResourceFleetServerHostDestroy(s *terraform.State) error { if err != nil { return err } - packagePolicy, diag := fleet.ReadFleetServerHost(context.Background(), fleetClient, rs.Primary.ID) - if diag.HasError() { - return errors.New(diag[0].Summary) + host, diags := fleet.ReadFleetServerHost(context.Background(), fleetClient, rs.Primary.ID) + if diags.HasError() { + return utils.FwDiagsAsError(diags) } - if packagePolicy != nil { - return fmt.Errorf("FleetServerHost id=%v still exists, but it should have been removed", rs.Primary.ID) + if host != nil { + return fmt.Errorf("fleet server host id=%v still exists, but it should have been removed", rs.Primary.ID) } } return nil diff --git a/internal/fleet/server_host/schema.go b/internal/fleet/server_host/schema.go new file mode 100644 index 000000000..44e6e8e25 --- /dev/null +++ b/internal/fleet/server_host/schema.go @@ -0,0 +1,42 @@ +package server_host + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *serverHostResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema.Description = "Creates a new Fleet Server Host." + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of this resource.", + Computed: true, + }, + "host_id": schema.StringAttribute{ + Description: "Unique identifier of the Fleet server host.", + Computed: true, + Optional: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the Fleet server host.", + Required: true, + }, + "hosts": schema.ListAttribute{ + Description: "A list of hosts.", + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "default": schema.BoolAttribute{ + Description: "Set as default.", + Optional: true, + }, + } +} diff --git a/internal/fleet/server_host/update.go b/internal/fleet/server_host/update.go new file mode 100644 index 000000000..96091c45f --- /dev/null +++ b/internal/fleet/server_host/update.go @@ -0,0 +1,46 @@ +package server_host + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients/fleet" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *serverHostResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var planModel serverHostModel + + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetFleetClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + hostID := planModel.HostID.ValueString() + body, diags := planModel.toAPIUpdateModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + host, diags := fleet.UpdateFleetServerHost(ctx, client, hostID, body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = planModel.populateFromAPI(ctx, host) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, planModel) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/utils/tfsdk.go b/internal/utils/tfsdk.go index 113002949..3f52577d1 100644 --- a/internal/utils/tfsdk.go +++ b/internal/utils/tfsdk.go @@ -23,6 +23,47 @@ func SliceToListType[T1 any, T2 any](ctx context.Context, value []T1, elemType a return list } +// 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, path path.Path, diags diag.Diagnostics) types.List { + return SliceToListType(ctx, value, types.StringType, path, diags, types.StringValue) +} + +// 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, path path.Path, diags diag.Diagnostics, iteratee func(item T1) T2) []T2 { + if !IsKnown(value) { + return nil + } + + elems := ListTypeAs[T1](ctx, value, path, diags) + if diags.HasError() { + return nil + } + + return TransformSlice(elems, iteratee) +} + +// 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, path path.Path, diags diag.Diagnostics) []string { + return ListTypeToSlice(ctx, value, path, diags, func(item types.String) string { + return item.ValueString() + }) +} + +// ListTypeAs converts a types.List into a tfsdk aware []T. +func ListTypeAs[T any](ctx context.Context, value types.List, path path.Path, diags diag.Diagnostics) []T { + if !IsKnown(value) { + return nil + } + + var items []T + nd := value.ElementsAs(ctx, &items, false) + diags.Append(ConvertToAttrDiags(nd, path)...) + return items +} + // 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 { diff --git a/internal/utils/tfsdk_test.go b/internal/utils/tfsdk_test.go index 2505b2419..329c590ee 100644 --- a/internal/utils/tfsdk_test.go +++ b/internal/utils/tfsdk_test.go @@ -2,6 +2,7 @@ package utils_test import ( "context" + "reflect" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/utils" @@ -11,77 +12,157 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +type naive struct { + ID string `json:"id"` +} +type aware struct { + ID types.String `tfsdk:"id"` +} + +var ( + naiveNil = ([]naive)(nil) + naiveEmpty = []naive{} + naiveFull = []naive{ + {ID: "id1"}, + {ID: "id2"}, + {ID: "id3"}, + } + + awareNil = ([]aware)(nil) + awareEmpty = []aware{} + awareFull = []aware{ + {ID: types.StringValue("id1")}, + {ID: types.StringValue("id2")}, + {ID: types.StringValue("id3")}, + } + + awareType = types.ObjectType{AttrTypes: map[string]attr.Type{"id": types.StringType}} + awareListUnk = types.ListUnknown(awareType) + awareListNil = types.ListNull(awareType) + awareListEmpty = types.ListValueMust(awareType, []attr.Value{}) + awareListFull = types.ListValueMust(awareType, []attr.Value{ + types.ObjectValueMust(awareType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id1")}), + types.ObjectValueMust(awareType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id2")}), + types.ObjectValueMust(awareType.AttrTypes, map[string]attr.Value{"id": types.StringValue("id3")}), + }) + + toNaive = func(item aware) naive { return naive{ID: item.ID.ValueString()} } + toAware = func(item naive) aware { return aware{ID: types.StringValue(item.ID)} } + + stringNil = ([]string)(nil) + stringEmpty = []string{} + stringFull = []string{"v1", "v2", "v3"} + + stringListUnk = types.ListUnknown(types.StringType) + stringListNil = types.ListNull(types.StringType) + stringListEmpty = types.ListValueMust(types.StringType, []attr.Value{}) + stringListFull = types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("v1"), + types.StringValue("v2"), + types.StringValue("v3"), + }) +) + func TestSliceToListType(t *testing.T) { t.Parallel() - type Type1 struct { - ID string `json:"id"` + tests := []struct { + name string + input []naive + want types.List + }{ + {name: "converts nil", input: naiveNil, want: awareListNil}, + {name: "converts empty", input: naiveEmpty, want: awareListEmpty}, + {name: "converts struct", input: naiveFull, want: awareListFull}, } - type Type2 struct { - ID types.String `tfsdk:"id"` + + 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, awareType, path.Empty(), diags, toAware) + 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()) + } + }) } - elemType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "id": types.StringType, - }, +} + +func TestSliceToListType_String(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []string + want types.List + }{ + {name: "converts nil", input: stringNil, want: stringListNil}, + {name: "converts empty", input: stringEmpty, want: stringListEmpty}, + {name: "converts strings", input: stringFull, want: stringListFull}, } - t1_t2 := func(item any) any { - i := item.(Type1) - return Type2{ - ID: types.StringValue(i.ID), - } + + ctx := context.Background() + + 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) + if !got.Equal(tt.want) { + t.Errorf("SliceToListType_String() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("SliceToListType_String() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) } - toString := func(item any) any { - return types.StringValue(item.(string)) +} + +func TestListTypeToSlice(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want []naive + input types.List + }{ + {name: "converts unknown", input: awareListUnk, want: naiveNil}, + {name: "converts nil", input: awareListNil, want: naiveNil}, + {name: "converts empty", input: awareListEmpty, want: naiveEmpty}, + {name: "converts struct", input: awareListFull, want: naiveFull}, } + ctx := context.Background() + + 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, toNaive) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ListTypeToSlice() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("ListTypeToSlice() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) + } +} + +func TestListTypeToSlice_String(t *testing.T) { + t.Parallel() + tests := []struct { - name string - input []any - want types.List - elemType attr.Type - iter func(any) any + name string + input types.List + want []string }{ - { - 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, - }, + {name: "converts unknown", input: stringListUnk, want: stringNil}, + {name: "converts nil", input: stringListNil, want: stringNil}, + {name: "converts empty", input: stringListEmpty, want: stringEmpty}, + {name: "converts strings", input: stringListFull, want: stringFull}, } ctx := context.Background() @@ -89,12 +170,69 @@ 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, tt.elemType, path.Empty(), diags, tt.iter) - if !got.Equal(tt.want) { - t.Errorf("SliceToListType() = %v, want %v", got, tt.want) + 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) } for _, d := range diags.Errors() { - t.Errorf("SlicetoListType() diagnostic: %s: %s", d.Summary(), d.Detail()) + t.Errorf("ListTypeToSlice_String() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) + } +} + +func TestListTypeAs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want []aware + input types.List + }{ + {name: "converts unknown", input: awareListUnk, want: awareNil}, + {name: "converts nil", input: awareListNil, want: awareNil}, + {name: "converts empty", input: awareListEmpty, want: awareEmpty}, + {name: "converts struct", input: awareListFull, want: awareFull}, + } + + ctx := context.Background() + + 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) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ListTypeAs() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("ListTypeAs() diagnostic: %s: %s", d.Summary(), d.Detail()) + } + }) + } +} + +func TestTransformSlice(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []naive + want []aware + }{ + {name: "converts nil", input: naiveNil, want: awareNil}, + {name: "converts empty", input: naiveEmpty, want: awareEmpty}, + {name: "converts struct", input: naiveFull, want: awareFull}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var diags diag.Diagnostics + got := utils.TransformSlice(tt.input, toAware) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("TransformSlice() = %v, want %v", got, tt.want) + } + for _, d := range diags.Errors() { + t.Errorf("TransformSlice() diagnostic: %s: %s", d.Summary(), d.Detail()) } }) } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index fb802190d..da514a6a1 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -256,6 +256,15 @@ func SdkDiagsAsError(diags sdkdiag.Diagnostics) error { return nil } +func FwDiagsAsError(diags fwdiag.Diagnostics) error { + for _, diag := range diags { + if diag.Severity() == fwdiag.SeverityError { + 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 diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 57d18b011..d997ade43 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -7,7 +7,11 @@ 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/agent_policy" "github.com/elastic/terraform-provider-elasticstack/internal/fleet/enrollment_tokens" + "github.com/elastic/terraform-provider-elasticstack/internal/fleet/integration" + "github.com/elastic/terraform-provider-elasticstack/internal/fleet/integration_ds" + "github.com/elastic/terraform-provider-elasticstack/internal/fleet/server_host" "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" @@ -74,6 +78,7 @@ func (p *Provider) DataSources(ctx context.Context) []func() datasource.DataSour indices.NewDataSource, spaces.NewDataSource, enrollment_tokens.NewDataSource, + integration_ds.NewDataSource, } } @@ -84,5 +89,8 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { func() resource.Resource { return &private_location.Resource{} }, func() resource.Resource { return &index.Resource{} }, func() resource.Resource { return &synthetics.Resource{} }, + agent_policy.NewResource, + integration.NewResource, + server_host.NewResource, } } diff --git a/provider/provider.go b/provider/provider.go index c3562774e..07adff0c9 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -81,8 +81,6 @@ func New(version string) *schema.Provider { "elasticstack_kibana_action_connector": kibana.DataSourceConnector(), "elasticstack_kibana_security_role": kibana.DataSourceRole(), - - "elasticstack_fleet_integration": fleet.DataSourceIntegration(), }, ResourcesMap: map[string]*schema.Resource{ "elasticstack_elasticsearch_cluster_settings": cluster.ResourceSettings(), @@ -110,10 +108,7 @@ func New(version string) *schema.Provider { "elasticstack_kibana_security_role": kibana.ResourceRole(), "elasticstack_kibana_slo": kibana.ResourceSlo(), - "elasticstack_fleet_agent_policy": fleet.ResourceAgentPolicy(), "elasticstack_fleet_output": fleet.ResourceOutput(), - "elasticstack_fleet_server_host": fleet.ResourceFleetServerHost(), - "elasticstack_fleet_integration": fleet.ResourceIntegration(), "elasticstack_fleet_integration_policy": fleet.ResourceIntegrationPolicy(), }, }