diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb4858d4..61c5a646d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Fix validation of `throttle`, and `interval` attributes in `elasticstack_kibana_alerting_rule` allowing all Elastic duration values ([#846](https://github.com/elastic/terraform-provider-elasticstack/pull/846)) - Fix boolean setting parsing for `elasticstack_elasticsearch_indices` data source. ([#842](https://github.com/elastic/terraform-provider-elasticstack/pull/842)) - Update all Fleet and utils/tfsdk instances of diagnostics parameters to pass by pointer instead of pass by value. Added upgrader for fleet_integration_policy v0 to handle empty string vars_json/streams_json. ([#855](https://github.com/elastic/terraform-provider-elasticstack/pull/855)) +- Fix handling of EPM packages when uninstalled outside Terraform, and diags in create/update. ([#854](https://github.com/elastic/terraform-provider-elasticstack/pull/854)) ## [0.11.9] - 2024-10-14 diff --git a/generated/fleet/fleet.gen.go b/generated/fleet/fleet.gen.go index 135643e8d..d602582fb 100644 --- a/generated/fleet/fleet.gen.go +++ b/generated/fleet/fleet.gen.go @@ -50,6 +50,27 @@ const ( Transform ElasticsearchAssetType = "transform" ) +// Defines values for GetPackageItemConditionsElasticsearchSubscription. +const ( + GetPackageItemConditionsElasticsearchSubscriptionBasic GetPackageItemConditionsElasticsearchSubscription = "basic" + GetPackageItemConditionsElasticsearchSubscriptionEnterprise GetPackageItemConditionsElasticsearchSubscription = "enterprise" + GetPackageItemConditionsElasticsearchSubscriptionGold GetPackageItemConditionsElasticsearchSubscription = "gold" + GetPackageItemConditionsElasticsearchSubscriptionPlatinum GetPackageItemConditionsElasticsearchSubscription = "platinum" +) + +// Defines values for GetPackageItemRelease. +const ( + GetPackageItemReleaseBeta GetPackageItemRelease = "beta" + GetPackageItemReleaseExperimental GetPackageItemRelease = "experimental" + GetPackageItemReleaseGa GetPackageItemRelease = "ga" +) + +// Defines values for GetPackageItemSourceLicense. +const ( + GetPackageItemSourceLicenseApache20 GetPackageItemSourceLicense = "Apache-2.0" + GetPackageItemSourceLicenseElastic20 GetPackageItemSourceLicense = "Elastic-2.0" +) + // Defines values for KibanaSavedObjectType. const ( CspRuleTemplate KibanaSavedObjectType = "csp_rule_template" @@ -123,23 +144,23 @@ const ( // Defines values for PackageInfoConditionsElasticsearchSubscription. const ( - Basic PackageInfoConditionsElasticsearchSubscription = "basic" - Enterprise PackageInfoConditionsElasticsearchSubscription = "enterprise" - Gold PackageInfoConditionsElasticsearchSubscription = "gold" - Platinum PackageInfoConditionsElasticsearchSubscription = "platinum" + PackageInfoConditionsElasticsearchSubscriptionBasic PackageInfoConditionsElasticsearchSubscription = "basic" + PackageInfoConditionsElasticsearchSubscriptionEnterprise PackageInfoConditionsElasticsearchSubscription = "enterprise" + PackageInfoConditionsElasticsearchSubscriptionGold PackageInfoConditionsElasticsearchSubscription = "gold" + PackageInfoConditionsElasticsearchSubscriptionPlatinum PackageInfoConditionsElasticsearchSubscription = "platinum" ) // Defines values for PackageInfoRelease. const ( - Beta PackageInfoRelease = "beta" - Experimental PackageInfoRelease = "experimental" - Ga PackageInfoRelease = "ga" + PackageInfoReleaseBeta PackageInfoRelease = "beta" + PackageInfoReleaseExperimental PackageInfoRelease = "experimental" + PackageInfoReleaseGa PackageInfoRelease = "ga" ) // Defines values for PackageInfoSourceLicense. const ( - Apache20 PackageInfoSourceLicense = "Apache-2.0" - Elastic20 PackageInfoSourceLicense = "Elastic-2.0" + PackageInfoSourceLicenseApache20 PackageInfoSourceLicense = "Apache-2.0" + PackageInfoSourceLicenseElastic20 PackageInfoSourceLicense = "Elastic-2.0" ) // Defines values for PackageInstallSource. @@ -281,6 +302,76 @@ type FleetServerHost struct { Name *string `json:"name,omitempty"` } +// GetPackageItem defines model for get_package_item. +type GetPackageItem struct { + Categories []string `json:"categories"` + Conditions struct { + Elasticsearch *struct { + Subscription *GetPackageItemConditionsElasticsearchSubscription `json:"subscription,omitempty"` + } `json:"elasticsearch,omitempty"` + Kibana *struct { + Versions *string `json:"versions,omitempty"` + } `json:"kibana,omitempty"` + } `json:"conditions"` + DataStreams *[]struct { + IngesetPipeline string `json:"ingeset_pipeline"` + Name string `json:"name"` + Package string `json:"package"` + Release string `json:"release"` + Title string `json:"title"` + Type string `json:"type"` + Vars *[]struct { + Default string `json:"default"` + Name string `json:"name"` + } `json:"vars,omitempty"` + } `json:"data_streams,omitempty"` + Description string `json:"description"` + Download string `json:"download"` + Elasticsearch *struct { + Privileges *struct { + Cluster *[]string `json:"cluster,omitempty"` + } `json:"privileges,omitempty"` + } `json:"elasticsearch,omitempty"` + FormatVersion string `json:"format_version"` + Internal *bool `json:"internal,omitempty"` + KeepPoliciesUpToDate *bool `json:"keepPoliciesUpToDate,omitempty"` + LatestVersion *string `json:"latestVersion,omitempty"` + LicensePath *string `json:"licensePath,omitempty"` + Name string `json:"name"` + Notice *string `json:"notice,omitempty"` + Path string `json:"path"` + Readme *string `json:"readme,omitempty"` + + // Release release label is deprecated, derive from the version instead (packages follow semver) + // Deprecated: + Release *GetPackageItemRelease `json:"release,omitempty"` + // Deprecated: + SavedObject map[string]interface{} `json:"savedObject"` + Screenshots *[]struct { + Path string `json:"path"` + Size *string `json:"size,omitempty"` + Src string `json:"src"` + Title *string `json:"title,omitempty"` + Type *string `json:"type,omitempty"` + } `json:"screenshots,omitempty"` + Source *struct { + License *GetPackageItemSourceLicense `json:"license,omitempty"` + } `json:"source,omitempty"` + Status PackageStatus `json:"status"` + Title string `json:"title"` + Type string `json:"type"` + Version string `json:"version"` +} + +// GetPackageItemConditionsElasticsearchSubscription defines model for GetPackageItem.Conditions.Elasticsearch.Subscription. +type GetPackageItemConditionsElasticsearchSubscription string + +// GetPackageItemRelease release label is deprecated, derive from the version instead (packages follow semver) +type GetPackageItemRelease string + +// GetPackageItemSourceLicense defines model for GetPackageItem.Source.License. +type GetPackageItemSourceLicense string + // GetPackagesResponse defines model for get_packages_response. type GetPackagesResponse struct { Items []SearchResult `json:"items"` @@ -592,7 +683,6 @@ type OutputUpdateRequestLogstashType string // PackageInfo defines model for package_info. type PackageInfo struct { - Assets []string `json:"assets"` Categories []string `json:"categories"` Conditions struct { Elasticsearch *struct { @@ -621,12 +711,11 @@ type PackageInfo struct { Cluster *[]string `json:"cluster,omitempty"` } `json:"privileges,omitempty"` } `json:"elasticsearch,omitempty"` - FormatVersion string `json:"format_version"` - Icons *[]string `json:"icons,omitempty"` - Internal *bool `json:"internal,omitempty"` - Name string `json:"name"` - Path string `json:"path"` - Readme *string `json:"readme,omitempty"` + FormatVersion string `json:"format_version"` + Internal *bool `json:"internal,omitempty"` + Name string `json:"name"` + Path string `json:"path"` + Readme *string `json:"readme,omitempty"` // Release release label is deprecated, derive from the version instead (packages follow semver) // Deprecated: @@ -3393,14 +3482,7 @@ type GetPackageResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Item *PackageInfo `json:"item,omitempty"` - KeepPoliciesUpToDate *bool `json:"keepPoliciesUpToDate,omitempty"` - LatestVersion *string `json:"latestVersion,omitempty"` - LicensePath *string `json:"licensePath,omitempty"` - Notice *string `json:"notice,omitempty"` - // Deprecated: - SavedObject map[string]interface{} `json:"savedObject"` - Status PackageStatus `json:"status"` + Item *GetPackageItem `json:"item,omitempty"` } JSON400 *Error } @@ -4344,14 +4426,7 @@ func ParseGetPackageResponse(rsp *http.Response) (*GetPackageResponse, error) { switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - Item *PackageInfo `json:"item,omitempty"` - KeepPoliciesUpToDate *bool `json:"keepPoliciesUpToDate,omitempty"` - LatestVersion *string `json:"latestVersion,omitempty"` - LicensePath *string `json:"licensePath,omitempty"` - Notice *string `json:"notice,omitempty"` - // Deprecated: - SavedObject map[string]interface{} `json:"savedObject"` - Status PackageStatus `json:"status"` + Item *GetPackageItem `json:"item,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err diff --git a/generated/fleet/getschema.go b/generated/fleet/getschema.go index b08753c35..548d9cd14 100644 --- a/generated/fleet/getschema.go +++ b/generated/fleet/getschema.go @@ -73,6 +73,7 @@ var transformers = []TransformFunc{ transformAddPackagePolicyVars, transformAddPackagePolicySecretReferences, transformFixPackageSearchResult, + transformFixGetPackageResult, } // transformFilterPaths filters the paths in a schema down to @@ -232,16 +233,36 @@ func transformInlinePackageDefinitions(schema *Schema) { // Get { - props, ok := epmPath.Get.Responses.GetFields("200.content.application/json.schema.allOf.1.properties") + respSchema, ok := epmPath.Get.Responses.GetFields("200.content.application/json.schema") if !ok { panic("properties not found") } - // status needs to be moved to schemes and a ref inserted in its place. - value, _ := props.Get("status") + // allOf.1 should also be inside "item" + _allOf, _ := respSchema.Get("allOf") + allOf := _allOf.([]any) + respSchema.Delete("allOf") + + list := make([]any, 2) + allOf0, _ := Fields(allOf[0].(map[string]any)).GetFields("properties.item") + allOf1 := Fields(allOf[1].(map[string]any)) + list[0] = allOf0 + list[1] = allOf1 + respSchema.Set("properties.item.allOf", list) + + // item needs to be moved to schemas and a ref inserted in its place. + value, _ := respSchema.Get("properties.item.allOf") + respSchema.Delete("properties.item.allOf") + schema.Components.Set("schemas.get_package_item.allOf", value) + respSchema.Set("properties.item.$ref", "#/components/schemas/get_package_item") + + // status needs to be moved to schemas and a ref inserted in its place. + props, _ := schema.Components.GetFields("schemas.get_package_item.allOf.1.properties") + value, _ = props.GetFields("status") schema.Components.Set("schemas.package_status", value) props.Delete("status") props.Set("status.$ref", "#/components/schemas/package_status") + } // Post @@ -369,6 +390,18 @@ func transformFixPackageSearchResult(schema *Schema) { properties.Delete("installationInfo") } +// transformFixGetPackageResult removes unneeded fields from the +// GetPackageResult struct. These fields are also causing parsing errors. +func transformFixGetPackageResult(schema *Schema) { + properties, ok := schema.Components.GetFields("schemas.package_info.properties") + if !ok { + panic("properties not found") + } + properties.Delete("assets") + properties.Delete("icons") + properties.Delete("installationInfo") +} + // downloadFile will download a file from url and return the // bytes. If the request fails, or a non 200 error code is // observed in the response, an error is returned instead. @@ -462,11 +495,15 @@ func (f Fields) Get(key string) (any, bool) { if !ok { return nil, false } - if m, isMap := slicedValue.(map[string]any); ok && isMap { + + switch m := slicedValue.(type) { + case map[string]any: return Fields(m).Get(postSliceKeys) + case Fields: + return m.Get(postSliceKeys) + default: + return slicedValue, true } - return slicedValue, true - default: rootKey = key } diff --git a/internal/clients/fleet/fleet.go b/internal/clients/fleet/fleet.go index 1a0905ceb..2ce1230a2 100644 --- a/internal/clients/fleet/fleet.go +++ b/internal/clients/fleet/fleet.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "net/http" fleetapi "github.com/elastic/terraform-provider-elasticstack/generated/fleet" @@ -333,27 +332,21 @@ 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) (*fleetapi.GetPackageItem, diag.Diagnostics) { params := fleetapi.GetPackageParams{} - resp, err := client.API.GetPackage(ctx, name, version, ¶ms) + resp, err := client.API.GetPackageWithResponse(ctx, name, version, ¶ms) if err != nil { - return utils.FrameworkDiagFromError(err) + return nil, utils.FrameworkDiagFromError(err) } - defer resp.Body.Close() - switch resp.StatusCode { + switch resp.StatusCode() { case http.StatusOK: - return nil + return resp.JSON200.Item, nil case http.StatusNotFound: - return utils.FrameworkDiagFromError(ErrPackageNotFound) + return nil, utils.FrameworkDiagFromError(ErrPackageNotFound) default: - errData, err := io.ReadAll(resp.Body) - if err != nil { - return utils.FrameworkDiagFromError(err) - } - - return reportUnknownError(resp.StatusCode, errData) + return nil, reportUnknownError(resp.StatusCode(), resp.Body) } } @@ -365,22 +358,16 @@ func InstallPackage(ctx context.Context, client *Client, name, version string, f IgnoreConstraints: nil, } - resp, err := client.API.InstallPackage(ctx, name, version, ¶ms, body) + resp, err := client.API.InstallPackageWithResponse(ctx, name, version, ¶ms, body) if err != nil { return utils.FrameworkDiagFromError(err) } - defer resp.Body.Close() - switch resp.StatusCode { + switch resp.StatusCode() { case http.StatusOK: return nil default: - errData, err := io.ReadAll(resp.Body) - if err != nil { - return utils.FrameworkDiagFromError(err) - } - - return reportUnknownError(resp.StatusCode, errData) + return reportUnknownError(resp.StatusCode(), resp.Body) } } @@ -399,6 +386,13 @@ func Uninstall(ctx context.Context, client *Client, name, version string, force switch resp.StatusCode() { case http.StatusOK: return nil + case http.StatusBadRequest: + msg := resp.JSON400.Message + if msg != nil && *msg == fmt.Sprintf("%s is not installed", name) { + return nil + } else { + return reportUnknownError(resp.StatusCode(), resp.Body) + } case http.StatusNotFound: return nil default: diff --git a/internal/fleet/integration/read.go b/internal/fleet/integration/read.go index fbd345851..96b759c04 100644 --- a/internal/fleet/integration/read.go +++ b/internal/fleet/integration/read.go @@ -3,6 +3,7 @@ package integration 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-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -25,12 +26,16 @@ func (r *integrationResource) Read(ctx context.Context, req resource.ReadRequest name := stateModel.Name.ValueString() version := stateModel.Version.ValueString() - diags = fleet.ReadPackage(ctx, client, name, version) + pkg, diags := fleet.ReadPackage(ctx, client, name, version) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { resp.State.RemoveResource(ctx) return } + if pkg.Status != fleetapi.Installed { + resp.State.RemoveResource(ctx) + return + } stateModel.ID = types.StringValue(getPackageID(name, version)) diff --git a/internal/fleet/integration/resource_test.go b/internal/fleet/integration/resource_test.go index 87826e7b8..a2d540743 100644 --- a/internal/fleet/integration/resource_test.go +++ b/internal/fleet/integration/resource_test.go @@ -1,13 +1,17 @@ package integration_test import ( + "context" "regexp" "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/versionutils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/stretchr/testify/require" ) var minVersionIntegration = version.Must(version.NewVersion("8.6.0")) @@ -68,6 +72,42 @@ func TestAccResourceIntegration(t *testing.T) { }) } +func TestAccResourceIntegrationDeleted(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + Config: testAccResourceIntegrationDeleted, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "name", "sysmon_linux"), + resource.TestCheckResourceAttr("elasticstack_fleet_integration.test_integration", "version", "1.7.0"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionIntegration), + Config: testAccResourceIntegrationDeleted, + // Force uninstall the integration + PreConfig: func() { + client, err := clients.NewAcceptanceTestingClient() + require.NoError(t, err) + + fleetClient, err := client.GetFleetClient() + require.NoError(t, err) + + ctx := context.Background() + diags := fleet.Uninstall(ctx, fleetClient, "sysmon_linux", "1.7.0", true) + require.Empty(t, diags) + }, + // Expect the plan to want to reinstall + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + const testAccResourceIntegration = ` provider "elasticstack" { elasticsearch {} @@ -81,3 +121,17 @@ resource "elasticstack_fleet_integration" "test_integration" { skip_destroy = true } ` + +const testAccResourceIntegrationDeleted = ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_fleet_integration" "test_integration" { + name = "sysmon_linux" + version = "1.7.0" + force = true + skip_destroy = false +} +`