diff --git a/.go-version b/.go-version index d28b1eb8..ca8ec414 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.22.9 +1.23.5 diff --git a/docs/data-sources/ai_config.md b/docs/data-sources/ai_config.md new file mode 100644 index 00000000..61a9dc5e --- /dev/null +++ b/docs/data-sources/ai_config.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "launchdarkly_ai_config Data Source - launchdarkly" +subcategory: "" +description: |- + Provides a LaunchDarkly AI Config data source. + This data source allows you to retrieve AI Config information from your LaunchDarkly project. + -> Note: AI Configs are currently in beta. +--- + +# launchdarkly_ai_config (Data Source) + +Provides a LaunchDarkly AI Config data source. + +This data source allows you to retrieve AI Config information from your LaunchDarkly project. + +-> **Note:** AI Configs are currently in beta. + + + + +## Schema + +### Required + +- `key` (String) The unique key of the AI Config. +- `project_key` (String) The project key. + +### Read-Only + +- `description` (String) The description of the AI Config. +- `id` (String) The ID of this resource. +- `maintainer_id` (String) The ID of the member who maintains this AI Config. +- `maintainer_team_key` (String) The key of the team that maintains this AI Config. +- `name` (String) The human-readable name of the AI Config. +- `tags` (Set of String) Tags associated with the AI Config. +- `version` (Number) The version of the AI Config. diff --git a/docs/resources/ai_config.md b/docs/resources/ai_config.md new file mode 100644 index 00000000..31df81f0 --- /dev/null +++ b/docs/resources/ai_config.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "launchdarkly_ai_config Resource - launchdarkly" +subcategory: "" +description: |- + Provides a LaunchDarkly AI Config resource. + This resource allows you to create and manage AI Configs within your LaunchDarkly project. + -> Note: AI Configs are currently in beta. +--- + +# launchdarkly_ai_config (Resource) + +Provides a LaunchDarkly AI Config resource. + +This resource allows you to create and manage AI Configs within your LaunchDarkly project. + +-> **Note:** AI Configs are currently in beta. + + + + +## Schema + +### Required + +- `key` (String) The unique key of the AI Config. +- `name` (String) The human-readable name of the AI Config. +- `project_key` (String) The project key. + +### Optional + +- `description` (String) The description of the AI Config. +- `maintainer_id` (String) The ID of the member who maintains this AI Config. +- `maintainer_team_key` (String) The key of the team that maintains this AI Config. +- `tags` (Set of String) Tags associated with the AI Config. + +### Read-Only + +- `id` (String) The ID of this resource. +- `version` (Number) The version of the AI Config. diff --git a/go.mod b/go.mod index ecd528bb..7810d572 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( ) require ( - github.com/launchdarkly/api-client-go/v17 v17.1.0 + github.com/launchdarkly/api-client-go/v17 v17.2.0 golang.org/x/sync v0.16.0 ) diff --git a/go.sum b/go.sum index dd7b82d7..0378642e 100644 --- a/go.sum +++ b/go.sum @@ -542,6 +542,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/launchdarkly/api-client-go/v17 v17.1.0 h1:IbR5UDLKBmff0eRBSD3UgVDfgnifOepBIe4gqivMaec= github.com/launchdarkly/api-client-go/v17 v17.1.0/go.mod h1:lMTmhEjepXfam8xm8b0ERBJbV9g8vdu9nbKueDXcB5o= +github.com/launchdarkly/api-client-go/v17 v17.2.0 h1:5CJxDaL7ZgqALAcohNUMlV7hfXR65s2czZ4XmZjW/qI= +github.com/launchdarkly/api-client-go/v17 v17.2.0/go.mod h1:lMTmhEjepXfam8xm8b0ERBJbV9g8vdu9nbKueDXcB5o= github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.3.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= github.com/leonklingele/grouper v1.1.0/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= diff --git a/launchdarkly/config.go b/launchdarkly/config.go index 1febdc3f..58bf899d 100644 --- a/launchdarkly/config.go +++ b/launchdarkly/config.go @@ -35,6 +35,8 @@ type Client struct { // ld is the standard API client that we use in most cases to interact with LaunchDarkly's APIs. ld *ldapi.APIClient + ldBeta *ldapi.APIClient + // ld404Retry is the same as ld except that it will also retry 404s with an exponential backoff. In most cases `ld` should be used instead. sc-218015 ld404Retry *ldapi.APIClient ctx context.Context @@ -73,12 +75,23 @@ func newLDClientConfig(apiHost string, httpTimeoutSeconds int, apiVersion string return cfg } +func newLDClientConfigNoVersion(apiHost string, httpTimeoutSeconds int, retryPolicy retryablehttp.CheckRetry) *ldapi.Configuration { + cfg := ldapi.NewConfiguration() + cfg.Host = apiHost + cfg.DefaultHeader = make(map[string]string) + cfg.UserAgent = fmt.Sprintf("launchdarkly-terraform-provider/%s", version) + cfg.HTTPClient = newRetryableClient(retryPolicy) + cfg.HTTPClient.Timeout = time.Duration(httpTimeoutSeconds) * time.Second + return cfg +} + func baseNewClient(token string, apiHost string, oauth bool, httpTimeoutSeconds int, apiVersion string, maxConcurrent int) (*Client, error) { if token == "" { return nil, errors.New("token cannot be empty") } standardConfig := newLDClientConfig(apiHost, httpTimeoutSeconds, apiVersion, standardRetryPolicy) + betaConfigNoVersion := newLDClientConfigNoVersion(apiHost, httpTimeoutSeconds, standardRetryPolicy) configWith404Retries := newLDClientConfig(apiHost, httpTimeoutSeconds, apiVersion, retryPolicyWith404Retries) ctx := context.WithValue(context.Background(), ldapi.ContextAPIKeys, map[string]ldapi.APIKey{ @@ -97,6 +110,7 @@ func baseNewClient(token string, apiHost string, oauth bool, httpTimeoutSeconds apiKey: token, apiHost: apiHost, ld: ldapi.NewAPIClient(standardConfig), + ldBeta: ldapi.NewAPIClient(betaConfigNoVersion), ld404Retry: ldapi.NewAPIClient(configWith404Retries), ctx: ctx, fallbackClient: fallbackClient, diff --git a/launchdarkly/data_source_launchdarkly_ai_config.go b/launchdarkly/data_source_launchdarkly_ai_config.go new file mode 100644 index 00000000..cdae8573 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_ai_config.go @@ -0,0 +1,69 @@ +package launchdarkly + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAIConfig() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceAIConfigRead, + Schema: map[string]*schema.Schema{ + PROJECT_KEY: { + Type: schema.TypeString, + Required: true, + Description: "The project key.", + }, + KEY: { + Type: schema.TypeString, + Required: true, + Description: "The unique key of the AI Config.", + }, + NAME: { + Type: schema.TypeString, + Computed: true, + Description: "The human-readable name of the AI Config.", + }, + DESCRIPTION: { + Type: schema.TypeString, + Computed: true, + Description: "The description of the AI Config.", + }, + TAGS: { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Tags associated with the AI Config.", + }, + MAINTAINER_ID: { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the member who maintains this AI Config.", + }, + MAINTAINER_TEAM_KEY: { + Type: schema.TypeString, + Computed: true, + Description: "The key of the team that maintains this AI Config.", + }, + VERSION: { + Type: schema.TypeInt, + Computed: true, + Description: "The version of the AI Config.", + }, + }, + Description: `Provides a LaunchDarkly AI Config data source. + +This data source allows you to retrieve AI Config information from your LaunchDarkly project. + +-> **Note:** AI Configs are currently in beta.`, + } +} + +func dataSourceAIConfigRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + projectKey := d.Get(PROJECT_KEY).(string) + key := d.Get(KEY).(string) + d.SetId(projectKey + "/" + key) + return resourceAIConfigRead(ctx, d, metaRaw) +} diff --git a/launchdarkly/data_source_launchdarkly_ai_config_test.go b/launchdarkly/data_source_launchdarkly_ai_config_test.go new file mode 100644 index 00000000..78690323 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_ai_config_test.go @@ -0,0 +1,118 @@ +package launchdarkly + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + testAccDataSourceAIConfig = ` +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" + description = "Test description" + tags = ["terraform", "test"] +} + +data "launchdarkly_ai_config" "test" { + project_key = launchdarkly_ai_config.test.project_key + key = launchdarkly_ai_config.test.key +} +` + + testAccDataSourceAIConfigWithTeamFmt = ` +resource "launchdarkly_team" "test" { + key = "%s" + name = "Test Team" +} + +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" + maintainer_team_key = launchdarkly_team.test.key +} + +data "launchdarkly_ai_config" "test" { + project_key = launchdarkly_ai_config.test.project_key + key = launchdarkly_ai_config.test.key +} +` +) + +func TestAccDataSourceAIConfig_exists(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "data.launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccDataSourceAIConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, KEY, "test-ai-config"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Test description"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttrSet(resourceName, VERSION), + ), + }, + }, + }) +} + +func TestAccDataSourceAIConfig_existsWithTeamMaintainer(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + teamKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "data.launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, fmt.Sprintf(testAccDataSourceAIConfigWithTeamFmt, teamKey)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, KEY, "test-ai-config"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, MAINTAINER_TEAM_KEY, teamKey), + resource.TestCheckResourceAttrSet(resourceName, VERSION), + ), + }, + }, + }) +} + +func TestAccDataSourceAIConfig_noMatchReturnsError(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + aiConfigKey := acctest.RandStringFromCharSet(24, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, fmt.Sprintf(` +data "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "%s" +} +`, aiConfigKey)), + ExpectError: regexp.MustCompile(`failed to get AI config`), + }, + }, + }) +} diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go index ceadc232..009e261e 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go @@ -156,8 +156,8 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) { require.NoError(t, err) }() - thisConfig := flag.Environments[envKey] - otherConfig := flag.Environments["production"] + thisConfig := (*flag.Environments)[envKey] + otherConfig := (*flag.Environments)["production"] flagId := projectKey + "/" + flagKey resourceName := "data.launchdarkly_feature_flag_environment.test" @@ -283,8 +283,8 @@ func TestAccDataSourceFeatureFlagEnvironment_WithContextFields(t *testing.T) { require.NoError(t, err) }() - thisConfig := flag.Environments[envKey] - otherConfig := flag.Environments["production"] + thisConfig := (*flag.Environments)[envKey] + otherConfig := (*flag.Environments)["production"] flagId := projectKey + "/" + flagKey resourceName := "data.launchdarkly_feature_flag_environment.test" diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index 4886d834..bd70ac59 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -119,7 +119,17 @@ func featureFlagEnvironmentRead(ctx context.Context, d *schema.ResourceData, raw return diag.Errorf("failed to get flag %q of project %q: %s", flagKey, projectKey, handleLdapiErr(err)) } - environment, ok := flag.Environments[envKey] + if flag.Environments == nil { + log.Printf("[WARN] failed to find environments for flag %q, removing from state", flagKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find environments for flag %q, removing from state", flagKey), + }) + d.SetId("") + return diags + } + + environment, ok := (*flag.Environments)[envKey] if !ok { log.Printf("[WARN] failed to find environment %q for flag %q, removing from state", envKey, flagKey) diags = append(diags, diag.Diagnostic{ diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index e32f5454..2a945130 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -77,6 +77,7 @@ func Provider() *schema.Provider { "launchdarkly_audit_log_subscription": resourceAuditLogSubscription(), "launchdarkly_relay_proxy_configuration": resourceRelayProxyConfig(), "launchdarkly_metric": resourceMetric(), + "launchdarkly_ai_config": resourceAIConfig(), }, DataSourcesMap: map[string]*schema.Resource{ "launchdarkly_team": dataSourceTeam(), @@ -92,6 +93,7 @@ func Provider() *schema.Provider { "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), "launchdarkly_relay_proxy_configuration": dataSourceRelayProxyConfig(), "launchdarkly_metric": dataSourceMetric(), + "launchdarkly_ai_config": dataSourceAIConfig(), }, ConfigureContextFunc: providerConfigure, } diff --git a/launchdarkly/resource_launchdarkly_ai_config.go b/launchdarkly/resource_launchdarkly_ai_config.go new file mode 100644 index 00000000..7601b938 --- /dev/null +++ b/launchdarkly/resource_launchdarkly_ai_config.go @@ -0,0 +1,277 @@ +package launchdarkly + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + ldapi "github.com/launchdarkly/api-client-go/v17" +) + +func resourceAIConfig() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAIConfigCreate, + ReadContext: resourceAIConfigRead, + UpdateContext: resourceAIConfigUpdate, + DeleteContext: resourceAIConfigDelete, + Exists: resourceAIConfigExists, + + Importer: &schema.ResourceImporter{ + State: resourceAIConfigImport, + }, + + Schema: map[string]*schema.Schema{ + PROJECT_KEY: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The project key.", + }, + KEY: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The unique key of the AI Config.", + }, + NAME: { + Type: schema.TypeString, + Required: true, + Description: "The human-readable name of the AI Config.", + }, + DESCRIPTION: { + Type: schema.TypeString, + Optional: true, + Description: "The description of the AI Config.", + }, + TAGS: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Tags associated with the AI Config.", + }, + MAINTAINER_ID: { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{MAINTAINER_TEAM_KEY}, + Description: "The ID of the member who maintains this AI Config.", + }, + MAINTAINER_TEAM_KEY: { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{MAINTAINER_ID}, + Description: "The key of the team that maintains this AI Config.", + }, + VERSION: { + Type: schema.TypeInt, + Computed: true, + Description: "The version of the AI Config.", + }, + }, + + Description: `Provides a LaunchDarkly AI Config resource. + +This resource allows you to create and manage AI Configs within your LaunchDarkly project. + +-> **Note:** AI Configs are currently in beta.`, + } +} + +func resourceAIConfigCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + + if exists, err := projectExists(projectKey, client); !exists { + if err != nil { + return diag.FromErr(err) + } + return diag.Errorf("cannot find project with key %q", projectKey) + } + + key := d.Get(KEY).(string) + name := d.Get(NAME).(string) + description := d.Get(DESCRIPTION).(string) + tags := stringsFromResourceData(d, TAGS) + + aiConfigPost := ldapi.AIConfigPost{ + Key: key, + Name: name, + Tags: tags, + } + + if description != "" { + aiConfigPost.Description = &description + } + + maintainerId, maintainerIdOk := d.GetOk(MAINTAINER_ID) + maintainerTeamKey, maintainerTeamKeyOk := d.GetOk(MAINTAINER_TEAM_KEY) + + if maintainerIdOk { + maintainerIdStr := maintainerId.(string) + aiConfigPost.MaintainerId = &maintainerIdStr + } + if maintainerTeamKeyOk { + maintainerTeamKeyStr := maintainerTeamKey.(string) + aiConfigPost.MaintainerTeamKey = &maintainerTeamKeyStr + } + + var err error + err = client.withConcurrency(ctx, func() error { + _, _, err = client.ldBeta.AIConfigsBetaApi.PostAIConfig(client.ctx, projectKey).LDAPIVersion("beta").AIConfigPost(aiConfigPost).Execute() + return err + }) + + if err != nil { + return diag.Errorf("failed to create AI config %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + } + + d.SetId(projectKey + "/" + key) + return resourceAIConfigRead(ctx, d, metaRaw) +} + +func resourceAIConfigRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + key := d.Get(KEY).(string) + + var aiConfig *ldapi.AIConfig + var res *http.Response + var err error + err = client.withConcurrency(ctx, func() error { + aiConfig, res, err = client.ldBeta.AIConfigsBetaApi.GetAIConfig(client.ctx, projectKey, key).LDAPIVersion("beta").Execute() + return err + }) + + if isStatusNotFound(res) { + d.SetId("") + return diags + } + if err != nil { + return diag.Errorf("failed to get AI config %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + } + + _ = d.Set(NAME, aiConfig.Name) + _ = d.Set(DESCRIPTION, aiConfig.Description) + _ = d.Set(TAGS, aiConfig.Tags) + _ = d.Set(VERSION, aiConfig.Version) + + if aiConfig.Maintainer != nil { + if aiConfig.Maintainer.MaintainerMember != nil { + _ = d.Set(MAINTAINER_ID, aiConfig.Maintainer.MaintainerMember.Id) + } + if aiConfig.Maintainer.AiConfigsMaintainerTeam != nil { + _ = d.Set(MAINTAINER_TEAM_KEY, aiConfig.Maintainer.AiConfigsMaintainerTeam.Key) + } + } + + return diags +} + +func resourceAIConfigUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + key := d.Get(KEY).(string) + name := d.Get(NAME).(string) + description := d.Get(DESCRIPTION).(string) + tags := stringsFromResourceData(d, TAGS) + + aiConfigPatch := ldapi.AIConfigPatch{ + Name: &name, + Description: &description, + Tags: tags, + } + + if d.HasChange(MAINTAINER_ID) || d.HasChange(MAINTAINER_TEAM_KEY) { + maintainerId, maintainerIdOk := d.GetOk(MAINTAINER_ID) + maintainerTeamKey, maintainerTeamKeyOk := d.GetOk(MAINTAINER_TEAM_KEY) + + if maintainerIdOk { + maintainerIdStr := maintainerId.(string) + aiConfigPatch.MaintainerId = &maintainerIdStr + } + if maintainerTeamKeyOk { + maintainerTeamKeyStr := maintainerTeamKey.(string) + aiConfigPatch.MaintainerTeamKey = &maintainerTeamKeyStr + } + } + + var err error + err = client.withConcurrency(ctx, func() error { + _, _, err = client.ldBeta.AIConfigsBetaApi.PatchAIConfig(client.ctx, projectKey, key).LDAPIVersion("beta").AIConfigPatch(aiConfigPatch).Execute() + return err + }) + + if err != nil { + return diag.Errorf("failed to update AI config %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + } + + return resourceAIConfigRead(ctx, d, metaRaw) +} + +func resourceAIConfigDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + key := d.Get(KEY).(string) + + var err error + err = client.withConcurrency(ctx, func() error { + _, err = client.ldBeta.AIConfigsBetaApi.DeleteAIConfig(client.ctx, projectKey, key).LDAPIVersion("beta").Execute() + return err + }) + + if err != nil { + return diag.Errorf("failed to delete AI config %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + } + + return diags +} + +func resourceAIConfigExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + key := d.Get(KEY).(string) + + var res *http.Response + var err error + err = client.withConcurrency(client.ctx, func() error { + _, res, err = client.ldBeta.AIConfigsBetaApi.GetAIConfig(client.ctx, projectKey, key).LDAPIVersion("beta").Execute() + return err + }) + + if isStatusNotFound(res) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to check if AI config %q exists in project %q: %s", key, projectKey, handleLdapiErr(err)) + } + + return true, nil +} + +func resourceAIConfigImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + id := d.Id() + + projectKey, aiConfigKey, err := aiConfigIdToKeys(id) + if err != nil { + return nil, err + } + _ = d.Set(PROJECT_KEY, projectKey) + _ = d.Set(KEY, aiConfigKey) + + return []*schema.ResourceData{d}, nil +} + +func aiConfigIdToKeys(id string) (projectKey string, aiConfigKey string, err error) { + if strings.Count(id, "/") != 1 { + return "", "", fmt.Errorf("found unexpected AI config id format: %q expected format: 'project_key/ai_config_key'", id) + } + parts := strings.SplitN(id, "/", 2) + projectKey, aiConfigKey = parts[0], parts[1] + return projectKey, aiConfigKey, nil +} diff --git a/launchdarkly/resource_launchdarkly_ai_config_test.go b/launchdarkly/resource_launchdarkly_ai_config_test.go new file mode 100644 index 00000000..34f98170 --- /dev/null +++ b/launchdarkly/resource_launchdarkly_ai_config_test.go @@ -0,0 +1,235 @@ +package launchdarkly + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + testAccAIConfigBasic = ` +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" +} +` + + testAccAIConfigUpdate = ` +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Updated AI Config" + description = "Updated description" + tags = ["terraform", "updated"] +} +` + + testAccAIConfigWithTags = ` +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" + description = "AI Config with tags" + tags = ["terraform", "test"] +} +` + + testAccAIConfigWithTeamMaintainerFmt = ` +resource "launchdarkly_team" "test" { + key = "%s" + name = "Test Team" +} + +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" + maintainer_team_key = launchdarkly_team.test.key +} +` + + testAccAIConfigConflictingMaintainersFmt = ` +resource "launchdarkly_team" "test" { + key = "%s" + name = "Test Team" +} + +resource "launchdarkly_ai_config" "test" { + project_key = launchdarkly_project.test.key + key = "test-ai-config" + name = "Test AI Config" + maintainer_id = "507f1f77bcf86cd799439011" + maintainer_team_key = launchdarkly_team.test.key +} +` +) + +func TestAccAIConfig_Create(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccAIConfigBasic), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckAIConfigExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, KEY, "test-ai-config"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), + resource.TestCheckResourceAttrSet(resourceName, VERSION), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAIConfig_Update(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccAIConfigBasic), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckAIConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, KEY, "test-ai-config"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), + ), + }, + { + Config: withRandomProject(projectKey, testAccAIConfigUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckAIConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated AI Config"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Updated description"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttrSet(resourceName, VERSION), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAIConfig_WithTags(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccAIConfigWithTags), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckAIConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "AI Config with tags"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAIConfig_WithTeamMaintainer(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + teamKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_ai_config.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, fmt.Sprintf(testAccAIConfigWithTeamMaintainerFmt, teamKey)), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckAIConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Test AI Config"), + resource.TestCheckResourceAttr(resourceName, MAINTAINER_TEAM_KEY, teamKey), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAIConfig_ConflictingMaintainers(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + teamKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, fmt.Sprintf(testAccAIConfigConflictingMaintainersFmt, teamKey)), + ExpectError: regexp.MustCompile(`(?i)maintainer_id.*conflicts.*maintainer_team_key`), + }, + }, + }) +} + +func testAccCheckAIConfigExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + if rs.Primary.ID == "" { + return fmt.Errorf("AI config ID is not set") + } + client := testAccProvider.Meta().(*Client) + projectKey, key, err := aiConfigIdToKeys(rs.Primary.ID) + if err != nil { + return err + } + _, _, err = client.ld.AIConfigsBetaApi.GetAIConfig(client.ctx, projectKey, key).Execute() + if err != nil { + return fmt.Errorf("received an error getting AI config: %s", err) + } + return nil + } +} diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go index 7d522b72..e2fe7b6f 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go @@ -1295,7 +1295,7 @@ func testAccCheckFeatureFlagEnvironmentDefaults(t *testing.T, projectKey, flagKe require.NoError(t, err) flag, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() require.NoError(t, err) - envConfig := flag.Environments["test"] + envConfig := (*flag.Environments)["test"] require.Equal(t, int32(0), *envConfig.OffVariation) return nil } diff --git a/launchdarkly/resource_launchdarkly_metric.go b/launchdarkly/resource_launchdarkly_metric.go index eee882ee..47b49989 100644 --- a/launchdarkly/resource_launchdarkly_metric.go +++ b/launchdarkly/resource_launchdarkly_metric.go @@ -191,7 +191,6 @@ func resourceMetricCreate(ctx context.Context, d *schema.ResourceData, metaRaw i kind := d.Get(KIND).(string) description := d.Get(DESCRIPTION).(string) tags := stringsFromResourceData(d, TAGS) - isActive := d.Get(IS_ACTIVE).(bool) isNumeric := d.Get(IS_NUMERIC).(bool) urls := metricUrlsFromResourceData(d) randomizationUnits := stringsFromResourceData(d, RANDOMIZATION_UNITS) @@ -210,7 +209,6 @@ func resourceMetricCreate(ctx context.Context, d *schema.ResourceData, metaRaw i Description: &description, Tags: tags, Kind: kind, - IsActive: &isActive, IsNumeric: &isNumeric, Selector: &selector, Urls: urls, @@ -300,7 +298,6 @@ func resourceMetricUpdate(ctx context.Context, d *schema.ResourceData, metaRaw i kind := d.Get(KIND).(string) description := d.Get(DESCRIPTION).(string) tags := stringsFromResourceData(d, TAGS) - isActive := d.Get(IS_ACTIVE).(bool) isNumeric := d.Get(IS_NUMERIC).(bool) urls := metricUrlsFromResourceData(d) // Required depending on type @@ -316,7 +313,6 @@ func resourceMetricUpdate(ctx context.Context, d *schema.ResourceData, metaRaw i patchReplace("/description", description), patchReplace("/tags", tags), patchReplace("/kind", kind), - patchReplace("/isActive", isActive), patchReplace("/isNumeric", isNumeric), patchReplace("/urls", urls), patchReplace("/unit", unit),