From be20145857a8bc65f2273187f6ea2a2f6c22d089 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Tue, 3 Aug 2021 11:10:06 +0200 Subject: [PATCH 01/36] Update CHANGELOG.md fix error in changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e7dcf6..bd529225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,11 +26,7 @@ NOTES: - The `launchdarkly_feature_flag_environment` resource's `targeting_enabled` argument has been deprecated in favor of `on`. Please update your config to use `on` in order to maintain compatibility with future versions. -<<<<<<< HEAD - -- # The `resource_launchdarkly_webhook` resource's `policy_statements` argument has been deprecated in favor of `inline_roles`. Please update your config to use `inline_roles` in order to maintain compatibility with future versions. - The `resource_launchdarkly_access_token` resource's `policy_statements` argument has been deprecated in favor of `inline_roles`. Please update your config to use `inline_roles` in order to maintain compatibility with future versions. - > > > > > > > 090c48d257abdc15f10ea4d4e9a8fab1321ad036 ## [1.6.0] (July 20, 2021) From b3dc2ca9d4ae8f9700f8bdba2c0eba2d6f282f58 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Thu, 5 Aug 2021 14:39:12 +0200 Subject: [PATCH 02/36] Imiller/ch117193/rename flag_fallthrough to fallthrough & user_targets to targets (#131) * add fallthrough and deprecate message to flag_fallthrough * update doc * conflicts with * handle * update a test * update changelog * fix typos in docs * add targets attribute * fix linter complaint * handle in create * update * handle read * update commetn * update tests * hack remove user_targets from plan * set both computed attributes in read * update changelog * always set * simplify helper * fix data source test * add test for deprecated field updates * fix fromResourceData functions * check renamed on as well * Update data source d oc * update data source tests --- CHANGELOG.md | 6 + ...nchdarkly_feature_flag_environment_test.go | 2 + launchdarkly/fallthrough_helper.go | 9 +- .../feature_flag_environment_helper.go | 31 +++- launchdarkly/keys.go | 6 +- .../resource_launchdarkly_feature_flag.go | 2 +- ...e_launchdarkly_feature_flag_environment.go | 18 +- ...nchdarkly_feature_flag_environment_test.go | 156 ++++++++++++++---- launchdarkly/target_helper.go | 17 +- .../d/feature_flag_environment.html.markdown | 31 ++-- .../r/feature_flag_environment.html.markdown | 16 +- 11 files changed, 215 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd529225..370cf35a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +NOTES: + +- The `launchdarkly_feature_flag_environment` resource and data source's `flag_fallthrough` argument has been deprecated in favor of `fallthrough`. Please update your config to use `fallthrough` in order to maintain compatibility with future versions. + +- The `launchdarkly_feature_flag_environment` resource and data source's `user_targets` argument has been deprecated in favor of `targets`. Please update your config to use `targets` in order to maintain compatibility with future versions. + ## [1.7.0] (August 2, 2021) FEATURES: diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go index 7742fdb9..25da4f7c 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go @@ -174,6 +174,8 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "prerequisites.0.variation", fmt.Sprint(thisConfig.Prerequisites[0].Variation)), resource.TestCheckResourceAttr(resourceName, "off_variation", fmt.Sprint(thisConfig.OffVariation)), // user targets will be two long because there is an empty one for the 0 value + resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", fmt.Sprint(len(thisConfig.Targets[0].Values))), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", fmt.Sprint(thisConfig.Fallthrough_.Variation)), resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.#", fmt.Sprint(len(thisConfig.Targets[0].Values))), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", fmt.Sprint(thisConfig.Fallthrough_.Variation)), ), diff --git a/launchdarkly/fallthrough_helper.go b/launchdarkly/fallthrough_helper.go index a1372c64..e48768d4 100644 --- a/launchdarkly/fallthrough_helper.go +++ b/launchdarkly/fallthrough_helper.go @@ -68,7 +68,14 @@ func isPercentRollout(fall []interface{}) bool { } func fallthroughFromResourceData(d *schema.ResourceData) (fallthroughModel, error) { - f := d.Get(FLAG_FALLTHROUGH).([]interface{}) + var f []interface{} + fallthroughHasChange := d.HasChange(FALLTHROUGH) + flagFallthroughHasChange := d.HasChange(FLAG_FALLTHROUGH) + if fallthroughHasChange { + f = d.Get(FALLTHROUGH).([]interface{}) + } else if flagFallthroughHasChange { + f = d.Get(FLAG_FALLTHROUGH).([]interface{}) + } err := validateFallThroughResourceData(f) if err != nil { return fallthroughModel{}, err diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index 2e08720e..f32bcef4 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -13,6 +13,16 @@ import ( ) func baseFeatureFlagEnvironmentSchema() map[string]*schema.Schema { + deprecatedFallthrough := fallthroughSchema() + deprecatedFallthrough.Deprecated = "'flag_fallthrough' is deprecated in favor of 'fallthrough'. This field will be removed in the next major release of the LaunchDarkly provider" + deprecatedFallthrough.ConflictsWith = []string{FALLTHROUGH} + newFallthrough := fallthroughSchema() + newFallthrough.ConflictsWith = []string{FLAG_FALLTHROUGH} + deprecatedTargets := targetsSchema() + deprecatedTargets.Deprecated = "'user_targets' is deprecated in favor of 'targets'. This field will be removed in the next major release of the LaunchDarkly provider" + deprecatedTargets.ConflictsWith = []string{TARGETS} + newTargets := targetsSchema() + newTargets.ConflictsWith = []string{USER_TARGETS} return map[string]*schema.Schema{ FLAG_ID: { Type: schema.TypeString, @@ -43,10 +53,12 @@ func baseFeatureFlagEnvironmentSchema() map[string]*schema.Schema { Computed: true, ConflictsWith: []string{TARGETING_ENABLED}, }, - USER_TARGETS: targetsSchema(), + USER_TARGETS: deprecatedTargets, + TARGETS: newTargets, RULES: rulesSchema(), PREREQUISITES: prerequisitesSchema(), - FLAG_FALLTHROUGH: fallthroughSchema(), + FLAG_FALLTHROUGH: deprecatedFallthrough, + FALLTHROUGH: newFallthrough, TRACK_EVENTS: { Type: schema.TypeBool, Optional: true, @@ -122,15 +134,26 @@ func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataS return fmt.Errorf("failed to set rules on flag with key %q: %v", flagKey, err) } - err = d.Set(USER_TARGETS, targetsToResourceData(environment.Targets)) + // user_targets is deprecated in favor of targets + err = d.Set(TARGETS, targetsToResourceData(environment.Targets)) if err != nil { return fmt.Errorf("failed to set targets on flag with key %q: %v", flagKey, err) } + err = d.Set(USER_TARGETS, targetsToResourceData(environment.Targets)) + if err != nil { + return fmt.Errorf("failed to set user_targets on flag with key %q: %v", flagKey, err) + } + // flag_fallthrough is deprecated in favor of fallthrough + err = d.Set(FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough_)) + if err != nil { + return fmt.Errorf("failed to set fallthrough on flag with key %q: %v", flagKey, err) + } err = d.Set(FLAG_FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough_)) if err != nil { - return fmt.Errorf("failed to set flag fallthrough on flag with key %q: %v", flagKey, err) + return fmt.Errorf("failed to set flag_fallthrough on flag with key %q: %v", flagKey, err) } + return nil } diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 9e48c65e..7e977fd2 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -59,13 +59,15 @@ const ( BUCKET_BY = "bucket_by" ROLLOUT_WEIGHTS = "rollout_weights" VARIATION = "variation" - USER_TARGETS = "user_targets" + USER_TARGETS = "user_targets" // deprecated + TARGETS = "targets" PREREQUISITES = "prerequisites" FLAG_KEY = "flag_key" TARGETING_ENABLED = "targeting_enabled" TRACK_EVENTS = "track_events" OFF_VARIATION = "off_variation" - FLAG_FALLTHROUGH = "flag_fallthrough" + FLAG_FALLTHROUGH = "flag_fallthrough" // deprecated + FALLTHROUGH = "fallthrough" KIND = "kind" CONFIG = "config" DEFAULT_ON_VARIATION = "default_on_variation" diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 9f0054e5..7d04518a 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -38,7 +38,7 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro if err != nil { return err } - return fmt.Errorf("Cannot find project with key %q", projectKey) + return fmt.Errorf("cannot find project with key %q", projectKey) } key := d.Get(KEY).(string) diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment.go b/launchdarkly/resource_launchdarkly_feature_flag_environment.go index e48a5057..8ec1ca86 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment.go @@ -97,14 +97,16 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf patches = append(patches, patchReplace(patchFlagEnvPath(d, "prerequisites"), prerequisites)) } - _, ok = d.GetOk(USER_TARGETS) - if ok { - targets := targetsFromResourceData(d, USER_TARGETS) + _, oldOk := d.GetOk(USER_TARGETS) + _, newOk := d.GetOk(TARGETS) + if oldOk || newOk { + targets := targetsFromResourceData(d) patches = append(patches, patchReplace(patchFlagEnvPath(d, "targets"), targets)) } - _, ok = d.GetOk(FLAG_FALLTHROUGH) - if ok { + _, newOk = d.GetOk(FALLTHROUGH) + _, oldOk = d.GetOk(FLAG_FALLTHROUGH) + if oldOk || newOk { fall, err := fallthroughFromResourceData(d) if err != nil { return err @@ -150,7 +152,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf if err != nil { return err } - return fmt.Errorf("Cannot find project with key %q", projectKey) + return fmt.Errorf("cannot find project with key %q", projectKey) } if exists, err := environmentExists(projectKey, envKey, client); !exists { @@ -167,7 +169,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf } trackEvents := d.Get(TRACK_EVENTS).(bool) prerequisites := prerequisitesFromResourceData(d, PREREQUISITES) - targets := targetsFromResourceData(d, USER_TARGETS) + targets := targetsFromResourceData(d) offVariation := d.Get(OFF_VARIATION).(int) fall, err := fallthroughFromResourceData(d) @@ -212,7 +214,7 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf if err != nil { return err } - return fmt.Errorf("Cannot find project with key %q", projectKey) + return fmt.Errorf("cannot find project with key %q", projectKey) } if exists, err := environmentExists(projectKey, envKey, client); !exists { diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go index 064c62d9..b701fc5d 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go @@ -38,6 +38,9 @@ resource "launchdarkly_feature_flag_environment" "basic" { flag_fallthrough { variation = 1 } + user_targets { + values = ["user1"] + } } ` @@ -86,13 +89,13 @@ resource "launchdarkly_feature_flag_environment" "basic" { env_key = "test" targeting_enabled = true track_events = true - user_targets { + targets { values = [] } - user_targets { + targets { values = ["user1", "user2"] } - user_targets { + targets { values = [] } rules { @@ -115,12 +118,49 @@ resource "launchdarkly_feature_flag_environment" "basic" { bucket_by = "email" } - flag_fallthrough { + fallthrough { rollout_weights = [60000, 40000, 0] bucket_by = "email" } } ` + testAccFeatureFlagEnvironmentUpdateDeprecated = ` +resource "launchdarkly_feature_flag" "basic" { + project_key = launchdarkly_project.test.key + key = "basic-flag" + name = "Basic feature flag" + variation_type = "number" + variations { + value = 0 + } + variations { + value = 10 + } + variations { + value = 30 + } +} + +resource "launchdarkly_feature_flag_environment" "basic" { + flag_id = launchdarkly_feature_flag.basic.id + env_key = "test" + targeting_enabled = true + track_events = true + user_targets { + values = ["user1", "user2"] + } + user_targets { + values = [] + } + user_targets { + values = [] + } + flag_fallthrough { + variation = 2 + } +} +` + testAccFeatureFlagEnvironmentJSONVariations = ` resource "launchdarkly_feature_flag" "json" { project_key = launchdarkly_project.test.key @@ -315,6 +355,8 @@ func TestAccFeatureFlagEnvironment_Basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), ), }, { @@ -359,7 +401,7 @@ func TestAccFeatureFlagEnvironment_Empty(t *testing.T) { }) } -func TestAccFeatureFlagEnvironment_Update(t *testing.T) { +func TestAccFeatureFlagEnvironment_UpdateDeprecatedFields(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_feature_flag_environment.basic" resource.ParallelTest(t, resource.TestCase{ @@ -373,32 +415,86 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "0"), + resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), resource.TestCheckResourceAttr(resourceName, "rules.#", "0"), ), }, { - Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdate), + Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdateDeprecated), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), + // computed values should come through resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "track_events", "true"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "2"), resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.#", "3"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.0", "60000"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.1", "40000"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.2", "0"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.bucket_by", "email"), + resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), resource.TestCheckResourceAttr(resourceName, "user_targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.#", "0"), resource.TestCheckResourceAttr(resourceName, "user_targets.2.values.#", "0"), + ), + }, + }, + }) +} + +func TestAccFeatureFlagEnvironment_Update(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag_environment.basic" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), + Check: resource.ComposeTestCheckFunc( + testAccCheckFeatureFlagEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "rules.#", "0"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckFeatureFlagEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "track_events", "true"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.#", "3"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.0", "60000"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.1", "40000"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.2", "0"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.bucket_by", "email"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), @@ -427,19 +523,19 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { testAccCheckFeatureFlagEnvironmentExists(resourceName), resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), resource.TestCheckResourceAttr(resourceName, "track_events", "true"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.#", "3"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.0", "60000"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.1", "40000"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout_weights.2", "0"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.bucket_by", "email"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.1", "user2"), - resource.TestCheckResourceAttr(resourceName, "user_targets.2.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.#", "3"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.0", "60000"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.1", "40000"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.2", "0"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.bucket_by", "email"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), diff --git a/launchdarkly/target_helper.go b/launchdarkly/target_helper.go index 2202a92a..9263105d 100644 --- a/launchdarkly/target_helper.go +++ b/launchdarkly/target_helper.go @@ -27,12 +27,15 @@ func targetsSchema() *schema.Schema { } } -func targetsFromResourceData(d *schema.ResourceData, metaRaw interface{}) []ldapi.Target { - tgts, ok := d.GetOk(USER_TARGETS) - if !ok { - return []ldapi.Target{} +func targetsFromResourceData(d *schema.ResourceData) []ldapi.Target { + var schemaTargets []interface{} + targetsHasChange := d.HasChange(TARGETS) + userTargetsHasChange := d.HasChange(USER_TARGETS) + if targetsHasChange { + schemaTargets = d.Get(TARGETS).([]interface{}) + } else if userTargetsHasChange { + schemaTargets = d.Get(USER_TARGETS).([]interface{}) } - schemaTargets := tgts.([]interface{}) targets := make([]ldapi.Target, len(schemaTargets)) for i, target := range schemaTargets { v := targetFromResourceData(i, target) @@ -58,9 +61,9 @@ func targetFromResourceData(variation int, val interface{}) ldapi.Target { return p } -// targetToResourceData converts the user_target information returned +// targetToResourceData converts the `target` information returned // by the LaunchDarkly API into a format suitable for Terraform -// If no user_targets are specified for a given variation, LaunchDarkly may +// If no `targets` are specified for a given variation, LaunchDarkly may // omit this information in the response. For example: // "targets": [ // { diff --git a/website/docs/d/feature_flag_environment.html.markdown b/website/docs/d/feature_flag_environment.html.markdown index 59974af7..a6ff47aa 100644 --- a/website/docs/d/feature_flag_environment.html.markdown +++ b/website/docs/d/feature_flag_environment.html.markdown @@ -38,11 +38,11 @@ In addition to the arguments above, the resource exports the following attribute - `prerequisites` - List of nested blocks describing prerequisite feature flags rules. To learn more, read [Nested Prequisites Blocks](#nested-prerequisites-blocks). -- `user_targets` - List of nested blocks describing the individual user targets for each variation. The order of the `user_targets` blocks determines the index of the variation to serve if a `user_target` is matched. To learn more, read [Nested User Target Blocks](#nested-user-targets-blocks). +- `targets` (previously `user_targets`) - List of nested blocks describing the individual user targets for each variation. The order of the `targets` blocks determines the index of the variation to serve if a `target` is matched. To learn more, read [Nested Target Blocks](#nested-targets-blocks). - `rules` - List of logical targeting rules. To learn more, read [Nested Rules Blocks](#nested-rules-blocks). -- `flag_fallthrough` - Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply. To learn more, read [Nested Flag Fallthrough Block](#nested-flag-fallthrough-block). +- `fallthrough` (previously `flag_fallthrough`) - Nested block describing the default variation to serve if no `prerequisites`, `target`, or `rules` apply. To learn more, read [Nested Fallthrough Block](#nested-fallthrough-block). ### Nested Prerequisites Blocks @@ -52,21 +52,21 @@ Nested `prerequisites` blocks have the following structure: - `variation` - The index of the prerequisite feature flag's variation targeted. -### Nested User Targets Blocks +### Nested Targets Blocks -Nested `user_targets` blocks have the following structure: +Nested `targets` blocks have the following structure: - `values` - List of `user` strings to target. -### Nested Flag Fallthrough Block +### Nested Fallthrough Block -The nested `flag_fallthrough` block has the following structure: +The nested `fallthrough` block has the following structure: -- `variation` - The default integer variation index served if no `prerequisites`, `user_target`, or `rules` apply. +- `variation` - The default integer variation index served if no `prerequisites`, `target`, or `rules` apply. -- `rollout_weights` - List of integer percentage rollout weights applied to each variation when no `prerequisites`, `user_target`, or `rules` apply. +- `rollout_weights` - List of integer percentage rollout weights applied to each variation when no `prerequisites`, `target`, or `rules` apply. -- `bucket_by` - Group percentage rollout by a custom attribute. +- `bucket_by` - Group percentage rollout by a custom attribute. ### Nested Rules Blocks @@ -74,11 +74,11 @@ Nested `rules` blocks have the following structure: - `clauses` - List of nested blocks specifying the logical clauses evaluated. To learn more, read [Nested Clauses Blocks](#nested-clauses-blocks). -- `variation` - The integer variation index served if the rule clauses evaluate to `true`. +- `variation` - The integer variation index served if the rule clauses evaluate to `true`. -- `rollout_weights` - List of integer percentage rollout weights applied to each variation when the rule clauses evaluates to `true`. +- `rollout_weights` - List of integer percentage rollout weights applied to each variation when the rule clauses evaluates to `true`. -- `bucket_by` - Group percentage rollout by a custom attribute. +- `bucket_by` - Group percentage rollout by a custom attribute. ### Nested Clauses Blocks @@ -94,9 +94,8 @@ Nested `clauses` blocks have the following structure: - `negate` - Whether the rule clause is negated. -Nested `flag_fallthrough` blocks have the following structure: +Nested `fallthrough` blocks have the following structure: -- `variation` - The integer variation index served when the rule clauses evaluate to `true`. - -- `rollout_weights` - List of integer percentage rollout weights applied to each variation when the rule clauses evaluates to `true`. +- `variation` - The integer variation index served when the rule clauses evaluate to `true`. +- `rollout_weights` - List of integer percentage rollout weights applied to each variation when the rule clauses evaluates to `true`. diff --git a/website/docs/r/feature_flag_environment.html.markdown b/website/docs/r/feature_flag_environment.html.markdown index 05f2538f..b0bbd38b 100644 --- a/website/docs/r/feature_flag_environment.html.markdown +++ b/website/docs/r/feature_flag_environment.html.markdown @@ -51,7 +51,7 @@ resource "launchdarkly_feature_flag_environment" "number_env" { variation = 0 } - flag_fallthrough { + fallthrough { rollout_weights = [60000, 40000, 0] } } @@ -77,7 +77,9 @@ resource "launchdarkly_feature_flag_environment" "number_env" { - `rules` - (Optional) List of logical targeting rules. To learn more, read [Nested Rules Blocks](#nested-rules-blocks). -- `flag_fallthrough` - (Optional) Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply. To learn more, read [Nested Flag Fallthrough Block](#nested-flag-fallthrough-block). +- `flag_fallthrough` - (Optional, **Deprecated**) Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply. This attribute is **deprecated** in favor of `fallthrough`. Please update all references of `flag_fallthrough` to `fallthrough` to maintain compatibility with future versions. + +- `fallthrough` - (Optional) Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply.To learn more, read [Nested Fallthrough Block](#nested-fallthrough-block). ### Nested Prerequisites Blocks @@ -93,9 +95,9 @@ Nested `user_targets` blocks have the following structure: - `values` - (Optional) List of `user` strings to target. -### Nested Flag Fallthrough Block +### Nested Fallthrough Block -The nested `flag_fallthrough` block has the following structure: +The nested `fallthrough` (previously `flag_fallthrough`) block has the following structure: - `variation` - (Optional) The default integer variation index to serve if no `prerequisites`, `user_target`, or `rules` apply. You must specify either `variation` or `rollout_weights`. @@ -129,12 +131,6 @@ Nested `clauses` blocks have the following structure: - `negate` - (Required) Whether to negate the rule clause. -Nested `flag_fallthrough` blocks have the following structure: - -- `variation` - (Optional) The integer variation index to serve if the rule clauses evaluate to `true`. You must specify either `variation` or `rollout_weights`. - -- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. - ## Attributes Reference In addition to the arguments above, the resource exports the following attribute: From 6922e7945532a038a9798ee049b877b95f8f502f Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Mon, 16 Aug 2021 15:14:54 +0100 Subject: [PATCH 03/36] Imiller/ch117375/GitHub issue empty string variation (#135) * should fix it * add test case * update changelog and doc --- CHANGELOG.md | 4 ++ ...resource_launchdarkly_feature_flag_test.go | 45 +++++++++++++++++++ launchdarkly/variations_helper.go | 11 ++++- website/docs/r/feature_flag.html.markdown | 8 ++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 370cf35a..89cf4918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [Unreleased] +BUG FIXES: + +- Fixes [a bug](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/60) where attempts to create `resource_launchdarkly_feature_flag` variations with an empty string value were throwing a panic. + NOTES: - The `launchdarkly_feature_flag_environment` resource and data source's `flag_fallthrough` argument has been deprecated in favor of `fallthrough`. Please update your config to use `fallthrough` in order to maintain compatibility with future versions. diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index d7aaac29..4ac85cb1 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -352,6 +352,20 @@ resource "launchdarkly_feature_flag" "defaults-multivariate" { value = "d" } } +` + testAccFeatureFlagEmptyStringVariation = ` +resource "launchdarkly_feature_flag" "empty_string_variation" { + project_key = launchdarkly_project.test.key + key = "empty-variation" + name = "string flag with empty string variation" + variation_type = "string" + variations { + value = "" + } + variations { + value = "non-empty" + } +} ` ) @@ -866,6 +880,37 @@ func TestAccFeatureFlag_UpdateMultivariateDefaults(t *testing.T) { }) } +func TestAccFeatureFlag_EmptyStringVariation(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag.empty_string_variation" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccFeatureFlagEmptyStringVariation), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", ""), + resource.TestCheckResourceAttr(resourceName, "variations.0.name", ""), + resource.TestCheckResourceAttr(resourceName, "variations.0.description", ""), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "non-empty"), + resource.TestCheckResourceAttr(resourceName, "variations.1.name", ""), + resource.TestCheckResourceAttr(resourceName, "variations.1.description", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCustomPropertyKey(key string, subKey string) string { return fmt.Sprintf("custom_properties.%d.%s", hashcode.String(key), subKey) } diff --git a/launchdarkly/variations_helper.go b/launchdarkly/variations_helper.go index 0cca7790..1b927202 100644 --- a/launchdarkly/variations_helper.go +++ b/launchdarkly/variations_helper.go @@ -176,8 +176,17 @@ func boolVariationFromResourceData(variation interface{}) ldapi.Variation { } func stringVariationFromResourceData(variation interface{}) ldapi.Variation { + var v interface{} + if variation == nil { // handle empty string value + v = "" + return ldapi.Variation{ + Name: "", + Description: "", + Value: &v, + } + } variationMap := variation.(map[string]interface{}) - v := variationMap[VALUE] + v = variationMap[VALUE] return ldapi.Variation{ Name: variationMap[NAME].(string), Description: variationMap[DESCRIPTION].(string), diff --git a/website/docs/r/feature_flag.html.markdown b/website/docs/r/feature_flag.html.markdown index 1508a5b2..644c95c3 100644 --- a/website/docs/r/feature_flag.html.markdown +++ b/website/docs/r/feature_flag.html.markdown @@ -104,6 +104,14 @@ Nested `variations` blocks have the following structure: - `value` - (Required) The variation value. The value's type must correspond to the `variation_type` argument. For example: `variation_type = "boolean"` accepts only `true` or `false`. The `"number"` variation type accepts both floats and ints, but please note that any trailing zeroes on floats will be trimmed (i.e. `1.1` and `1.100` will both be converted to `1.1`). +If you wish to define an empty string variation, you must still define the value field on the variations block like so: + +``` +variations { + value = "" +} +``` + - `name` - (Optional) The name of the variation. - `description` - (Optional) The variation's description. From 61c544d280d285b0a7f7fdbcf3269d9fbe78e9bc Mon Sep 17 00:00:00 2001 From: Sunny Guduru Date: Wed, 25 Aug 2021 15:16:49 -0700 Subject: [PATCH 04/36] Fix duplicate text in CHANGELOG.md Somehow the changelog test was duplicated. Removing, and we can fix the changelog on our next release. --- CHANGELOG.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7efd940..272a3d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,6 @@ NOTES: - The `launchdarkly_feature_flag_environment` resource and data source's `user_targets` argument has been deprecated in favor of `targets`. Please update your config to use `targets` in order to maintain compatibility with future versions. -BUG FIXES: - -- Fixes [a bug](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/60) where attempts to create `resource_launchdarkly_feature_flag` variations with an empty string value were throwing a panic. - -NOTES: - -- The `launchdarkly_feature_flag_environment` resource and data source's `flag_fallthrough` argument has been deprecated in favor of `fallthrough`. Please update your config to use `fallthrough` in order to maintain compatibility with future versions. - -- The `launchdarkly_feature_flag_environment` resource and data source's `user_targets` argument has been deprecated in favor of `targets`. Please update your config to use `targets` in order to maintain compatibility with future versions. - ## [1.7.0] (August 2, 2021) FEATURES: From 361e8c24cd90a47db159c29e364d6b0a3cf8060a Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Tue, 31 Aug 2021 13:15:19 +0200 Subject: [PATCH 05/36] V2 main (#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * destination on should default to false (v2) (#124) * remove enabled * test that on defaults to false when removed * Imiller/ch115579/fix include in snippet bug (#129) * all it needed was a default val * add test to ensure optional attributes get set correctly when removed * fix test * use TestCheckNoResourceAttr instead of TestCheckResourceAttr * update changelog * Imiller/ch113419/upgrade terraform client to v2 (#114) * run v2 helper * replace hashcode.String() with schema.HashString() * replace terraform.ResourceProvider with *schema.Provider * fix data source TypeMaps * bump v2 sdk to latest (#116) * merge * Remove versions.tf (#119) * Imiller/ch114162/upgrade resource launchdarkly environment (#121) * make project_key required * update descriptions * update changelog * Imiller/ch114165/v1 project audit (#120) * add descriptions * update doc dscription * audit team member for v1 (#122) * update descriptions * add import instructions to team member docs * add team member examples * ForceNew when email is updated * update test file structure to better reflect provider pattern * add AtLeastOneOf for role and custom_roles * oops wrong doc * use validation.StringInSlice() * refactor key conversion functions into helper file * remove owner * forgot changelog * Imiller/ch114163/v1 feature flag env (#112) * add descriptions for everything * fix dangling default values * fix bug where creating a non-bool flag with no variations creates a bool flag * add bug fixes to changelog * built in an idiotic bug, fixed now * see if this fixes all of the tests * found typo: * fix broken tests * add descriptions * update some descriptions for clarity * make clauses RequiredWith on variations to preempt api error * fix error in doc * fix docs properly * deprecated targeting_enabled in favor of on * remove ReuqiredWith because it's causing a bug * fix plan bug * hopefully this will fix it * ughhhhh it was that it needed to be computed * forgot the doc oops * forgot the changelog too meep * destination on should default to false (v2) (#124) * remove enabled * test that on defaults to false when removed * Imiller/ch114160/v1 custom role (#125) * add descriptions * reformat tests to match pattern * check removal of optional attributes removes * holy crap how did this not break earlier * not sure how that one stuck around either * somehow we were not actually setting the flag_id * fix feature flag read * fix csa on project read * apparently errors are different too * remove lingering merge conflict text from changelog * fix noMatchReturnsError errors * remove hash indices on type set * remove hash function for custom role policy * fix destination tests * fix feature flag tests * fix env tests * fix project tests * fix segment tests * fix segment tests and remove unused testAccTagKey function * revert computed * define CSA inline * update changelog * remove MaxItems property from data sources * fix forProject Co-authored-by: Henry Barrow * Imiller/ch115576/revert feature flag env optional attributes (#130) * remove deprecated attribute targeting_enabled (replaced by on) * on should default to false when removed * forgot to fix data source tests * fix test to handle on reversion * make rules not computed * make user_targets not computed * update comments * finally got fallthrough working * fix test * clarify * fix more tests * remove deprecated user_targets and flag_fallthrough from schema * update read * revert helpers * update create * update tests * update example * update doc * remove deprecated fields from keys * update changelog * fix fallthrough and fix test * update changelog * missed a bit on the docs' * make prerequisite not computed * udate changelog again * add test case for removing prereqs * update flag_id description for clarity * add defaults for optional fields * update test again * update changelog again * handle off_variation defaults * fix data source test * have to use GetOkExists instead of GetOk to be able to set 0 off_variation on create * remove commented-out code * Imiller/ch115576/make fallthrough off variation required (#134) * make required in schemas * fix some test configs * add required attributes to rest of tests. tests all failing * always set both on read * no need to check if fallthrough not set because tf will enforce * update CRU functions * fix data source tests * update changelog and docs for new required attributes * remove comment no longer applicable * fix typo * remove `enabled` on webhooks * removed `policy_statements` on webhooks * removed `targeting_enabled` on feature flag environment * update change log * Change schema function to use recommended one. * Imiller/ch119139/make env configs required on project resource (#137) * make required * delete envs that were previously in state and have been removed * remove redundant test & add required envs * ImportSTateVerifyIgnore envs * update docs * move full_config example into v1_full_config * add v2 full config * update example * update test project scaffolding function * change State to StateContext function * update comment * fix destination test bug * add test case for when we fix env defaults * add note to import about envs * update changelog * remove debug bin * add debug_bin to gitignore * address pr comments by adding back stuff to docs * update all example versions to 1.7 * Added logic to delete remote webhook statements when statements blockā€¦ (#133) * Added logic to delete remote webhook statements when statements block is removed * refactored tests * Added has changes check around the webhook policy statements * Reverted changes in version file * Removed check for deprecated policy_statements * Removed deprecated fields and refactored helper functions using those deprecated fields * Removed ConflictsWith for the webhook ON schema since ENABLED no longer exists in the schema * Fixed test cases * Updated changelog, refactored getWebhookOn and updated the docs * Updated version in example and added default config for webhook ON element * Imiller/ch119137/make segment clause negate field optional (#136) * make oiptional and default to false + update test * add example segment config for 2.0 * this should be v2.0.0 * Do not require fallthrough and off_variation in ff_env data source (#142) * added version to versions.tf file for webhook * v2 for custom_role and destination. Update read me * Change targets schema to set and add 'variation' attribute (#141) * Revise targets schema * update docs * Make target values required * Apply suggestions from code review Co-authored-by: Isabelle Miller * Change List to Set in description * remove debug_bin Co-authored-by: Isabelle Miller * Imiller/ch115572/restructure default flag variations (#146) * add new defaults to schema * update tests to expected configs / outcomes * add default on * restructure defaultVariationsFromResourceData * update test cases * hitting a non-empty plan error in test * ffs just needed to be Computed * update tests * update docs and changelog * fix test * lost a brace * fix variation helper * fix changelog * small doc fixes (#147) * remove enabled from destination docs * update webhook doc * update doc on name issue (#151) * environment and data_source_webhook examples (#149) * added more examples * added comments * Imiller/ch120560/feature flag maintainer id should be removed (#150) * make computed and update tes * update doc * update changelog * Imiller/ch119620/audit examples (#152) * make separate v1 and v2 feature flag examples * ensure v2 ff example works * move some stuff around * some more reorg * more reorg * update toc in readme * update v2 feature flag readme * Bug Fix: Revert optional env attributes to defaults (#148) * Fixed and tests passing * improve env update test * default_ttl default, and create new test * default_ttl should reset to 0 * update docs and changelog Co-authored-by: Isabelle Miller * remove test examples shouldn't have been checked in * add terraform version note to changelog Co-authored-by: Henry Barrow Co-authored-by: Sunny Co-authored-by: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> --- .gitignore | 4 +- CHANGELOG.md | 74 +++- examples/README.md | 20 +- examples/custom_role/example.tf | 4 - .../versions.tf | 7 +- examples/team_member/example.tf | 4 - examples/team_member/versions.tf | 1 + examples/{ => v1}/feature_flags/README.md | 14 +- .../feature_flags/flag_types_example.tf | 0 examples/{ => v1}/feature_flags/setup.tf | 2 +- .../feature_flags/targeting_example.tf | 18 +- examples/v1/feature_flags/versions.tf | 8 + examples/v1/full_config/README.md | 26 ++ examples/{ => v1}/full_config/env-dev.tf | 0 examples/{ => v1}/full_config/env-staging.tf | 2 +- examples/{ => v1}/full_config/flags.tf | 0 examples/{ => v1}/full_config/project.tf | 0 examples/{ => v1}/full_config/roles.tf | 0 examples/{ => v1}/multiple_projects/README.md | 0 .../{ => v1}/multiple_projects/example.tf | 4 +- .../data_source_destination/example.tf | 0 .../v2/data_source_destination/versions.tf | 9 + examples/v2/data_source_webhook/example.tf | 23 ++ examples/v2/environment/example.tf | 21 ++ examples/v2/feature_flags/README.md | 301 ++++++++++++++++ .../v2/feature_flags/flag_types_example.tf | 97 ++++++ examples/v2/feature_flags/setup.tf | 25 ++ .../v2/feature_flags/targeting_example.tf | 70 ++++ examples/v2/feature_flags/versions.tf | 8 + examples/{ => v2}/full_config/README.md | 11 +- examples/v2/full_config/flags.tf | 59 ++++ .../v2/full_config/production-env-configs.tf | 23 ++ examples/v2/full_config/project.tf | 31 ++ examples/v2/full_config/roles.tf | 36 ++ .../v2/full_config/staging-env-configs.tf | 25 ++ examples/v2/full_config/versions.tf | 9 + examples/v2/segment/example.tf | 35 ++ examples/v2/segment/versions.tf | 9 + examples/{ => v2}/webhook/README.md | 14 +- examples/{ => v2}/webhook/example.tf | 10 +- examples/v2/webhook/versions.tf | 9 + go.mod | 2 +- go.sum | 179 ++++------ launchdarkly/clause_helper.go | 7 +- launchdarkly/clause_helper_test.go | 2 +- launchdarkly/custom_properties_helper.go | 7 +- launchdarkly/custom_properties_helper_test.go | 2 +- .../data_source_launchdarkly_environment.go | 2 +- ...ta_source_launchdarkly_environment_test.go | 6 +- .../data_source_launchdarkly_feature_flag.go | 4 +- ...e_launchdarkly_feature_flag_environment.go | 4 +- ...nchdarkly_feature_flag_environment_test.go | 19 +- ...a_source_launchdarkly_feature_flag_test.go | 10 +- .../data_source_launchdarkly_project.go | 4 +- .../data_source_launchdarkly_project_test.go | 10 +- .../data_source_launchdarkly_segment.go | 2 +- .../data_source_launchdarkly_segment_test.go | 6 +- .../data_source_launchdarkly_team_member.go | 2 +- ...ta_source_launchdarkly_team_member_test.go | 4 +- .../data_source_launchdarkly_webhook.go | 7 +- .../data_source_launchdarkly_webhook_test.go | 11 +- launchdarkly/default_variations_helper.go | 93 +---- .../default_variations_helper_test.go | 123 ++----- launchdarkly/destination_helper.go | 2 +- launchdarkly/environments_helper.go | 20 +- launchdarkly/fallthrough_helper.go | 27 +- .../feature_flag_environment_helper.go | 68 ++-- launchdarkly/feature_flags_helper.go | 71 ++-- launchdarkly/keys.go | 7 +- launchdarkly/policies_helper.go | 5 +- launchdarkly/policy_statements_helper.go | 4 +- launchdarkly/policy_statements_helper_test.go | 2 +- launchdarkly/prerequisite_helper.go | 5 +- launchdarkly/project_helper.go | 10 +- launchdarkly/provider.go | 7 +- launchdarkly/provider_test.go | 17 +- .../resource_launchdarkly_access_token.go | 4 +- ...resource_launchdarkly_access_token_test.go | 12 +- .../resource_launchdarkly_custom_role.go | 2 +- .../resource_launchdarkly_custom_role_test.go | 41 +-- .../resource_launchdarkly_destination.go | 43 +-- .../resource_launchdarkly_destination_test.go | 60 ++-- .../resource_launchdarkly_environment.go | 2 +- .../resource_launchdarkly_environment_test.go | 94 +++-- .../resource_launchdarkly_feature_flag.go | 2 +- ...e_launchdarkly_feature_flag_environment.go | 62 +--- ...nchdarkly_feature_flag_environment_test.go | 329 ++++++++---------- ...resource_launchdarkly_feature_flag_test.go | 233 ++++++++----- launchdarkly/resource_launchdarkly_project.go | 88 +++-- .../resource_launchdarkly_project_test.go | 100 +++++- launchdarkly/resource_launchdarkly_segment.go | 2 +- .../resource_launchdarkly_segment_test.go | 23 +- .../resource_launchdarkly_team_member.go | 6 +- .../resource_launchdarkly_team_member_test.go | 17 +- launchdarkly/resource_launchdarkly_webhook.go | 72 +--- .../resource_launchdarkly_webhook_test.go | 66 ++-- launchdarkly/rollout_helper.go | 4 +- launchdarkly/rule_helper.go | 5 +- launchdarkly/segment_rule_helper.go | 4 +- launchdarkly/segments_helper.go | 2 +- launchdarkly/tags_helper.go | 2 +- launchdarkly/target_helper.go | 96 ++--- launchdarkly/target_helper_test.go | 18 +- launchdarkly/validation_helper.go | 4 +- launchdarkly/variations_helper.go | 4 +- launchdarkly/variations_helper_test.go | 2 +- launchdarkly/webhooks_helper.go | 53 +-- main.go | 2 +- website/docs/d/feature_flag.html.markdown | 15 +- .../d/feature_flag_environment.html.markdown | 6 +- website/docs/d/project.html.markdown | 2 + website/docs/d/webhook.html.markdown | 8 +- website/docs/r/destination.html.markdown | 4 +- website/docs/r/environment.html.markdown | 12 +- website/docs/r/feature_flag.html.markdown | 26 +- .../r/feature_flag_environment.html.markdown | 46 +-- website/docs/r/project.html.markdown | 20 +- website/docs/r/team_member.html.markdown | 4 +- website/docs/r/webhook.html.markdown | 14 +- 119 files changed, 2032 insertions(+), 1343 deletions(-) rename examples/{data_source_destination => custom_role}/versions.tf (50%) rename examples/{ => v1}/feature_flags/README.md (97%) rename examples/{ => v1}/feature_flags/flag_types_example.tf (100%) rename examples/{ => v1}/feature_flags/setup.tf (95%) rename examples/{ => v1}/feature_flags/targeting_example.tf (85%) create mode 100644 examples/v1/feature_flags/versions.tf create mode 100644 examples/v1/full_config/README.md rename examples/{ => v1}/full_config/env-dev.tf (100%) rename examples/{ => v1}/full_config/env-staging.tf (96%) rename examples/{ => v1}/full_config/flags.tf (100%) rename examples/{ => v1}/full_config/project.tf (100%) rename examples/{ => v1}/full_config/roles.tf (100%) rename examples/{ => v1}/multiple_projects/README.md (100%) rename examples/{ => v1}/multiple_projects/example.tf (98%) rename examples/{ => v2}/data_source_destination/example.tf (100%) create mode 100644 examples/v2/data_source_destination/versions.tf create mode 100644 examples/v2/data_source_webhook/example.tf create mode 100644 examples/v2/environment/example.tf create mode 100644 examples/v2/feature_flags/README.md create mode 100644 examples/v2/feature_flags/flag_types_example.tf create mode 100644 examples/v2/feature_flags/setup.tf create mode 100644 examples/v2/feature_flags/targeting_example.tf create mode 100644 examples/v2/feature_flags/versions.tf rename examples/{ => v2}/full_config/README.md (89%) create mode 100644 examples/v2/full_config/flags.tf create mode 100644 examples/v2/full_config/production-env-configs.tf create mode 100644 examples/v2/full_config/project.tf create mode 100644 examples/v2/full_config/roles.tf create mode 100644 examples/v2/full_config/staging-env-configs.tf create mode 100644 examples/v2/full_config/versions.tf create mode 100644 examples/v2/segment/example.tf create mode 100644 examples/v2/segment/versions.tf rename examples/{ => v2}/webhook/README.md (83%) rename examples/{ => v2}/webhook/example.tf (81%) create mode 100644 examples/v2/webhook/versions.tf diff --git a/.gitignore b/.gitignore index 57d850fd..22624338 100644 --- a/.gitignore +++ b/.gitignore @@ -34,11 +34,13 @@ /terraform-provider-launchdarkly /build/ /dist +/vendor/ +launchdarkly/__debug_bin website/vendor website/.vagrant website/.bundle website/build website/node_modules -.vscode/ \ No newline at end of file +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 272a3d48..85bc42f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,76 @@ ## [1.7.1] (August 24, 2021) +ENHANCEMENTS: + +- Improved test coverage. + +NOTES: + +- As part of the ongoing deprecation of Terraform 0.11, the LaunchDarkly provider now only supports Terraform 0.12 and higher. + +- This release changes the way LaunchDarkly recommends you manage `launchdarkly_environment` and `launchdarkly_project` resources in tandem. It is recommended you do not manage environments as separate resources _unless_ you wish to manage the encapsulating project externally (not via Terraform). As such, at least one `environments` attribute will now be `Required` on the `launchdarkly_project` resource, but you will also be able to manage environments outside of Terraform on Terraform-managed projects if you do not import them into the Terraform state as a configuration block on the encapsulating project resource. + +- The deprecated `launchdarkly_destination` resource `enabled` field has been removed in favor of `on`. `on` now defaults to `false` when not explicitly set. + +- The `default_on_variation` and `default_off_variation` properties on the `launchdarkly_feature_flag` resource have now been replaced with a computed `defaults` block containing the properties `on_variation` and `off_variation` that refer to the variations in question by index rather than value. + +- The `launchdarkly_feature_flag_environment` resource and data source `target` attribute schema has been modified to include a new `variation` attribute. Here `variation` represents the index of the feature flag variation to serve if a user target is matched. + +- The deprecated `launchdarkly_feature_flag_environment` resource `targeting_enabled` field has been removed in favor of `on`. `on` now defaults to `false` when not explicitly set. + +- The deprecated `launchdarkly_feature_flag_environment` resource `user_targets` field has been removed in favor of `targets`. `targets` now defaults to null when not explicitly set. + +- The deprecated `launchdarkly_feature_flag_environment` resource `flag_fallthrough` field has been removed in favor of `fallthrough`. + +- The deprecated `launchdarkly_webhooks` resource `enabled` field has been removed in favor of `on`. `on` is now a required field. + +- The deprecated `launchdarkly_webhooks` resource `policy_statements` field has been removed in favor of `statements`. + +- `off_variation` and `fallthrough` (previously `flag_fallthrough`) on `launchdarkly_feature_flag_environment` are now `Required` fields. + +- Most optional fields will now be removed or revert to their null / false value when not explicitly set and / or when removed, including: + + - `on` on the `launchdarkly_destination` resource + + - `include_in_snippet` on the `launchdarkly_project` resource + + - on the `launchdarkly_environment` resource and in `environment` blocks on the `launchdarkly_project` resource: + + - `secure_mode` + + - `default_track_events` + + - `require_comments` + + - `confirm_changes` + + - `default_ttl` (reverts to `0`) + + - on the `launchdarkly_feature_flag_environment` resource: + + - `on` (previously `targeting_enabled`, reverts to `false`) + + - `rules` + + - `targets` (previously `user_targets`) + + - `prerequisites` + + - `track_events` (reverts to `false`) + +BUG FIXES: + +- Fixed a bug in the `launchdarkly_webhook` resource where `statements` removed from the configuration were not being deleted in LaunchDarkly. + +- The `launchdarkly_feature_flag` resource `maintainer_id` field is now computed and will update the state with the most recently-set value when not explicitly set. + +- The `client_side_availability` attribute on the `launchdarkly_feature_flag` and `launchdarkly_project` data sources has been corrected to an array with a single map item. This means that you will need to add an index 0 when accessing this property from the state (ex. `client_side_availability.using_environment_id` will now have to be accessed as `client_side_availability.0.using_environment_id`). + +## [Unreleased] + BUG FIXES: -- Fixes [a bug](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/60) where attempts to create `resource_launchdarkly_feature_flag` variations with an empty string value were throwing a panic. +- Fixes [a bug](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/60) where attempts to create `launchdarkly_feature_flag` variations with an empty string value were throwing a panic. NOTES: @@ -36,7 +104,7 @@ NOTES: - The `launchdarkly_feature_flag_environment` resource's `targeting_enabled` argument has been deprecated in favor of `on`. Please update your config to use `on` in order to maintain compatibility with future versions. -- The `resource_launchdarkly_access_token` resource's `policy_statements` argument has been deprecated in favor of `inline_roles`. Please update your config to use `inline_roles` in order to maintain compatibility with future versions. +- The `launchdarkly_access_token` resource's `policy_statements` argument has been deprecated in favor of `inline_roles`. Please update your config to use `inline_roles` in order to maintain compatibility with future versions. ## [1.6.0] (July 20, 2021) @@ -229,7 +297,7 @@ BUG FIXES: FEATURES: -- Add tags attribute to `resource_launchdarkly_environment`. [#5](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/5) +- Add tags attribute to `launchdarkly_environment` resource. [#5](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/5) - Add `maintainer_id` input validation. ENHANCEMENTS: diff --git a/examples/README.md b/examples/README.md index ff3d5bf8..c526ba94 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,11 +12,21 @@ Before getting started with the LaunchDarkly Terraform provider, make sure you h ## Contents -- [full_config](./full_config) contains an example of a simple but fully fleshed-out LaunchDarkly [project](https://docs.launchdarkly.com/home/managing-flags/projects) configuration, including [environments](https://docs.launchdarkly.com/home/managing-flags/environments), [feature flags](https://docs.launchdarkly.com/home/managing-flags), [team members](https://docs.launchdarkly.com/home/account-security/managing-your-team), [roles](https://docs.launchdarkly.com/home/account-security/custom-roles), and [segments](https://docs.launchdarkly.com/home/managing-users/segments). It also provides an example of how to organize a more complex configuration with multiple resources. -- [multiple_projects](./multiple_projects) contains an example of the simultaneous configuration of multiple [projects](https://docs.launchdarkly.com/home/managing-flags/projects) and associated [environments](https://docs.launchdarkly.com/home/managing-flags/environments) and [flags](https://docs.launchdarkly.com/home/managing-flags) in a single file. -- [custom_role](./custom_role) contains an example of how to configure a [custom role](https://docs.launchdarkly.com/home/account-security/custom-roles) within LaunchDarkly using Terraform. -- [feature_flags](./feature_flags) contains a full range of flag examples, covering both [flag variation types](https://docs.launchdarkly.com/home/managing-flags/flag-variations) and complex [targeting rules](https://docs.launchdarkly.com/home/managing-flags/targeting-users). -- [webhook](./webhook) contains an example of a [LaunchDarkly webhook](https://docs.launchdarkly.com/integrations/webhooks) configuration to send LD event notifications to an external endpoint. +- [v1](./v1) contains examples of configurations compatible with v1.X of the LaunchDarkly provider. Please note that this version is no longer maintained. + - [v1/full_config](./v1/full_config) contains an example of a simple but fully fleshed-out LaunchDarkly [project](https://docs.launchdarkly.com/home/managing-flags/projects) configuration, including [environments](https://docs.launchdarkly.com/home/managing-flags/environments), [feature flags](https://docs.launchdarkly.com/home/managing-flags), [team members](https://docs.launchdarkly.com/home/account-security/managing-your-team), [roles](https://docs.launchdarkly.com/home/account-security/custom-roles), and [segments](https://docs.launchdarkly.com/home/managing-users/segments). It provides an example of how to organize a more complex configuration with multiple resources. + - [v1/multiple_projects](./v1/multiple_projects) contains an example of the simultaneous configuration of multiple [projects](https://docs.launchdarkly.com/home/managing-flags/projects) and associated [environments](https://docs.launchdarkly.com/home/managing-flags/environments) and [flags](https://docs.launchdarkly.com/home/managing-flags) in a single file. + - [v1/feature_flags](./v1/feature_flags) contains a full range of flag examples, covering both [flag variation types](https://docs.launchdarkly.com/home/managing-flags/flag-variations) and complex [targeting rules](https://docs.launchdarkly.com/home/managing-flags/targeting-users). +- [v2](./v2) contains examples of configurations compatible with v2+ of the LaunchDarkly provider. + - [v2/full_config](./v2/full_config) contains an example of a simple but fully fleshed-out LaunchDarkly [project](https://docs.launchdarkly.com/home/managing-flags/projects) configuration with nested environments, [feature flags](https://docs.launchdarkly.com/home/managing-flags), [team members](https://docs.launchdarkly.com/home/account-security/managing-your-team), [roles](https://docs.launchdarkly.com/home/account-security/custom-roles), and [segments](https://docs.launchdarkly.com/home/managing-users/segments). It provides an example of how to organize a more complex configuration with multiple resources. + - [v2/feature_flags](./v2/feature_flags) contains a full range of flag examples, covering both [flag variation types](https://docs.launchdarkly.com/home/managing-flags/flag-variations) and complex [targeting rules](https://docs.launchdarkly.com/home/managing-flags/targeting-users). + - [v2/environment](./v2/environment) contains an example of how to configure a standalone [LaunchDarkly environment](https://docs.launchdarkly.com/home/organize/environments) in Terraform. Please note that this is only recommended in v2 of the provider if you wish to manage the encapsulating project outside of Terraform. + - [v2/segment](./v2/segment) contains an example of a [LaunchDarkly segment](https://docs.launchdarkly.com/home/data-export/segment) configuration to send LD event notifications to an external endpoint. + - [v2/webhook](./v2/webhook) contains an example of a [LaunchDarkly webhook](https://docs.launchdarkly.com/integrations/webhooks) configuration to send LD event notifications to an external endpoint. + - [v2/data_source_destination](./v2/data_source_destination) provides an example of how to configure a [LaunchDarkly data export destination](https://docs.launchdarkly.com/home/data-export) data source for easy reference in other resources. + - [v2/data_source_webhook](./v2/data_source_webhook) provides an example of how to configure a [LaunchDarkly webhook](https://docs.launchdarkly.com/integrations/webhooks) data source for easy reference in other resources. +- [access_token](./access_token) contains an example of how to configure LaunchDarkly [access tokens](https://docs.launchdarkly.com/home/account-security/api-access-tokens) using Terraform. This configuration is compatible with both v1 and v2 of the provider. +- [custom_role](./custom_role) contains an example of how to configure a [custom role](https://docs.launchdarkly.com/home/account-security/custom-roles) within LaunchDarkly using Terraform. This configuration is compatible with both v1 and v2 of the provider. +- [team_member](./team_member) contains an example of how to configure a [team member](https://docs.launchdarkly.com/home/members/managing) within LaunchDarkly using Terraform. This configuration is compatible with both v1 and v2 of the provider. - For an example of how to configure your provider if using Terraform version 0.13 or above, please see the [terraform_0.13](./terraform_0.13). diff --git a/examples/custom_role/example.tf b/examples/custom_role/example.tf index c7cb2bac..d778f995 100644 --- a/examples/custom_role/example.tf +++ b/examples/custom_role/example.tf @@ -1,9 +1,5 @@ # This config provides an example of a custom role that prevents management of any flag # with a "terraform-managed" tag to ensure these are only managed via terraform. - -provider "launchdarkly" { - version = "~> 1.0" -} resource "launchdarkly_custom_role" "exclude_terraform" { key = "exclude-terraform" name = "Exclude Terraform" diff --git a/examples/data_source_destination/versions.tf b/examples/custom_role/versions.tf similarity index 50% rename from examples/data_source_destination/versions.tf rename to examples/custom_role/versions.tf index d62355bc..cd0e962a 100644 --- a/examples/data_source_destination/versions.tf +++ b/examples/custom_role/versions.tf @@ -2,13 +2,8 @@ terraform { required_providers { launchdarkly = { source = "launchdarkly/launchdarkly" - version = "~> 1.5.1" + version = "~> 2.0" } } required_version = ">= 0.13" } - -# provider "launchdarkly" { -# version = "~> 1.6.0" -# source = "local/terraform-provider-launchdarkly" -# } \ No newline at end of file diff --git a/examples/team_member/example.tf b/examples/team_member/example.tf index 80e62b79..85901f32 100644 --- a/examples/team_member/example.tf +++ b/examples/team_member/example.tf @@ -1,7 +1,3 @@ -provider "launchdarkly" { - version = ">= 1.6.0" -} - resource "launchdarkly_team_member" "joe" { email = "joe@example.com" first_name = "Joe" diff --git a/examples/team_member/versions.tf b/examples/team_member/versions.tf index 6867ea9d..7052a021 100644 --- a/examples/team_member/versions.tf +++ b/examples/team_member/versions.tf @@ -2,6 +2,7 @@ terraform { required_providers { launchdarkly = { source = "launchdarkly/launchdarkly" + version = ">= 1.7" } } required_version = ">= 0.13" diff --git a/examples/feature_flags/README.md b/examples/v1/feature_flags/README.md similarity index 97% rename from examples/feature_flags/README.md rename to examples/v1/feature_flags/README.md index 6ee10948..cabe2521 100644 --- a/examples/feature_flags/README.md +++ b/examples/v1/feature_flags/README.md @@ -5,11 +5,13 @@ The LaunchDarkly provider provides two resources for configuring feature flags: [`launchdarkly_feature_flag`](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag.html), which allows you to configure and manipulate project-wide feature flag settings and [`launchdarkly_feature_flag_environment`](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag_environment.html), which allows you to manage environment-specific feature flag settings, such as [targeting rules](https://docs.launchdarkly.com/home/managing-flags/targeting-users) and [prerequisites](https://docs.launchdarkly.com/home/managing-flags/flag-prerequisites). This example contains three config files: + - [setup.tf](./setup.tf), which auths the provider and creates a project under which the flags will be created - [flag_types_example.tf](./flag_types_example.tf), which provides examples of the different ways you can define binary (boolean) and multivariate (string, numeric, and JSON) flag variations using the `launchdarkly_feature_flag` resource - [targeting_example.tf](./targeting_example.tf), which provides complex examples of user targeting using the `launchdarkly_feature_flag_environment` resource. For more detail on user targeting, see the [official LaunchDarkly documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users). ### Run + Init your working directory from the CL with `terraform init` and then apply the changes with `terraform apply`. You should see output resembling the following: ``` @@ -42,10 +44,10 @@ Terraform will perform the following actions: + env_key = "test" + flag_id = (known after apply) + id = (known after apply) - + targeting_enabled = true + + on = true + track_events = true - + flag_fallthrough { + + fallthrough { + bucket_by = "company" + rollout_weights = [ + 60000, @@ -71,18 +73,18 @@ Terraform will perform the following actions: } } - + user_targets { + + targets { + values = [ + "test_user0", ] } - + user_targets { + + targets { + values = [ + "test_user1", + "test_user2", ] } - + user_targets { + + targets { + values = [ + "test_user3", ] @@ -148,4 +150,4 @@ To view your flags, navigate to the Feature flags section on the left sidebar an You should be able to view specific flag policies by clicking into them. Here you can see how the policies for the user_targeting_flag would look: -![user_targeting_flag policies](../assets/images/feature-flag-targeting.png) \ No newline at end of file +![user_targeting_flag policies](../assets/images/feature-flag-targeting.png) diff --git a/examples/feature_flags/flag_types_example.tf b/examples/v1/feature_flags/flag_types_example.tf similarity index 100% rename from examples/feature_flags/flag_types_example.tf rename to examples/v1/feature_flags/flag_types_example.tf diff --git a/examples/feature_flags/setup.tf b/examples/v1/feature_flags/setup.tf similarity index 95% rename from examples/feature_flags/setup.tf rename to examples/v1/feature_flags/setup.tf index 39e29354..13222357 100644 --- a/examples/feature_flags/setup.tf +++ b/examples/v1/feature_flags/setup.tf @@ -2,7 +2,7 @@ # since feature flags require association with a specific project provider "launchdarkly" { - version = ">= 1.2.0" + version = ">= 1.7" } # since all projects are automatically created with a "test" and "production" diff --git a/examples/feature_flags/targeting_example.tf b/examples/v1/feature_flags/targeting_example.tf similarity index 85% rename from examples/feature_flags/targeting_example.tf rename to examples/v1/feature_flags/targeting_example.tf index 833d974d..5ec15919 100644 --- a/examples/feature_flags/targeting_example.tf +++ b/examples/v1/feature_flags/targeting_example.tf @@ -7,7 +7,7 @@ resource "launchdarkly_feature_flag_environment" "prereq_flag" { flag_id = launchdarkly_feature_flag.number_flag.id env_key = "production" - targeting_enabled = true + on = true prerequisites { flag_key = launchdarkly_feature_flag.boolean_flag.key @@ -26,8 +26,6 @@ resource "launchdarkly_feature_flag_environment" "prereq_flag" { # This flag provides an example of user-specific targeting in the test environment # on the string_flag defined in "flag_types_example.tf". -# The order of the user_targets blocks determines the index of the variation -# to be served to each set of users. # The rules block of this resource determines that the 0-index variation ("string1") will # be served to users whose names start with the letters a-e. # flag_fallthrough describes the default to serve if none of the other rules apply: @@ -37,16 +35,16 @@ resource "launchdarkly_feature_flag_environment" "prereq_flag" { resource "launchdarkly_feature_flag_environment" "user_targeting_flag" { flag_id = launchdarkly_feature_flag.string_flag.id env_key = "test" - targeting_enabled = true + on = true track_events = true - user_targets { - values = ["test_user0"] + targets { + values = ["test_user0"] } - user_targets { - values = ["test_user1", "test_user2"] + targets { + values = ["test_user1", "test_user2"] } - user_targets { - values = ["test_user3"] + targets { + values = ["test_user3"] } rules { clauses { diff --git a/examples/v1/feature_flags/versions.tf b/examples/v1/feature_flags/versions.tf new file mode 100644 index 00000000..6867ea9d --- /dev/null +++ b/examples/v1/feature_flags/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + } + } + required_version = ">= 0.13" +} diff --git a/examples/v1/full_config/README.md b/examples/v1/full_config/README.md new file mode 100644 index 00000000..d1b1c7e3 --- /dev/null +++ b/examples/v1/full_config/README.md @@ -0,0 +1,26 @@ +## Example: full LD config + +### Introduction + +The LaunchDarkly Terraform provider allows you to configure a full suite of LaunchDarkly features using Terraform. Please note that the examples contained in this directory are compatible with an earlier version of our Terraform provider that is no longer maintained. For a more up-to-date example, see the [V2 configuration example](./../v2_full_config/). + +The sample configuration in this directory provides an example of how to organize a full project configuration that takes advantage of the following LaunchDarkly resource types: + +- [launchdarkly_project](https://www.terraform.io/docs/providers/launchdarkly/r/project.html) in [project.tf](./project.tf) +- [launchdarkly_environment](https://www.terraform.io/docs/providers/launchdarkly/r/environment.html) in [env-staging.tf](./env-staging.tf) and [env-dev.tf](./env-dev.tf) +- [launchdarkly_custom_role](https://www.terraform.io/docs/providers/launchdarkly/r/custom_role.html) and [launchdarkly_team_member](https://www.terraform.io/docs/providers/launchdarkly/r/team_member.html) in [roles.tf](./roles.tf) +- [launchdarkly_feature_flag](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag.html) and [launchdarkly_feature_flag_environment](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag_environment.html) in [flags.tf](./flags.tf) and [env-staging.tf](./env-staging.tf), respectively +- [launchdarkly_segment](https://www.terraform.io/docs/providers/launchdarkly/r/segment.html) in [env-dev.tf](./env-dev.tf) + +Resources not included in this example are: + +- [launchdarkly_webhook](https://www.terraform.io/docs/providers/launchdarkly/r/webhook.html), an example of which can be found in the [webhook](../webhook) directory +- [launchdarkly_destination](https://www.terraform.io/docs/providers/launchdarkly/r/destination.html) + +### Important notes + +- If you wish to use Terraform to configure existing LaunchDarkly resources, you will first need to import them from the CL using `terraform import`. For details on the precise syntax, select the relevant resource [from this page](https://www.terraform.io/docs/providers/launchdarkly/index.html) and scroll to the bottom. For example, if you want to configure the automatically-created production environment after creating the project, run `terraform import launchdarkly_environment.production tf-full-config/production` before running your env config files. + +### Run + +Init your working directory from the CL with `terraform init` and then apply the changes with `terraform apply`. If you are creating it for the first time, the output should end with something like `Plan: 11 to add, 0 to change, 0 to destroy.` diff --git a/examples/full_config/env-dev.tf b/examples/v1/full_config/env-dev.tf similarity index 100% rename from examples/full_config/env-dev.tf rename to examples/v1/full_config/env-dev.tf diff --git a/examples/full_config/env-staging.tf b/examples/v1/full_config/env-staging.tf similarity index 96% rename from examples/full_config/env-staging.tf rename to examples/v1/full_config/env-staging.tf index dec7a4b3..9b80ef5a 100644 --- a/examples/full_config/env-staging.tf +++ b/examples/v1/full_config/env-staging.tf @@ -15,7 +15,7 @@ resource "launchdarkly_environment" "staging" { resource "launchdarkly_feature_flag_environment" "ld_internal_tester_staging" { flag_id = launchdarkly_feature_flag.ld_internal_tester.id env_key = "staging" - targeting_enabled = true + on = true prerequisites { flag_key = launchdarkly_feature_flag.binary_flag.key diff --git a/examples/full_config/flags.tf b/examples/v1/full_config/flags.tf similarity index 100% rename from examples/full_config/flags.tf rename to examples/v1/full_config/flags.tf diff --git a/examples/full_config/project.tf b/examples/v1/full_config/project.tf similarity index 100% rename from examples/full_config/project.tf rename to examples/v1/full_config/project.tf diff --git a/examples/full_config/roles.tf b/examples/v1/full_config/roles.tf similarity index 100% rename from examples/full_config/roles.tf rename to examples/v1/full_config/roles.tf diff --git a/examples/multiple_projects/README.md b/examples/v1/multiple_projects/README.md similarity index 100% rename from examples/multiple_projects/README.md rename to examples/v1/multiple_projects/README.md diff --git a/examples/multiple_projects/example.tf b/examples/v1/multiple_projects/example.tf similarity index 98% rename from examples/multiple_projects/example.tf rename to examples/v1/multiple_projects/example.tf index eb85c39d..e2d25fe8 100644 --- a/examples/multiple_projects/example.tf +++ b/examples/v1/multiple_projects/example.tf @@ -4,7 +4,7 @@ # ----------------------------------------------------------------------------------- # # AUTH CONFIG provider "launchdarkly" { - version = "~> 1.0" + version = "~> 1.7" } # ----------------------------------------------------------------------------------- # @@ -59,7 +59,7 @@ resource "launchdarkly_feature_flag_environment" "basic_variation" { # key must be retrieved through the `launchdarkly_project` resource. env_key = launchdarkly_project.tf_project_1.environments.0.key - targeting_enabled = true + on = true rules { clauses { diff --git a/examples/data_source_destination/example.tf b/examples/v2/data_source_destination/example.tf similarity index 100% rename from examples/data_source_destination/example.tf rename to examples/v2/data_source_destination/example.tf diff --git a/examples/v2/data_source_destination/versions.tf b/examples/v2/data_source_destination/versions.tf new file mode 100644 index 00000000..cd0e962a --- /dev/null +++ b/examples/v2/data_source_destination/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } + required_version = ">= 0.13" +} diff --git a/examples/v2/data_source_webhook/example.tf b/examples/v2/data_source_webhook/example.tf new file mode 100644 index 00000000..39821f9c --- /dev/null +++ b/examples/v2/data_source_webhook/example.tf @@ -0,0 +1,23 @@ +// Use the webhook datasource to grab existing webhook information from LaunchDarkly. +// All you need to provide is the id of the webhook. + +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } + required_version = ">= 0.13" +} + +// Get data from LaunchDarkly on an existing webhook, all you need to do is provide the id +data "launchdarkly_webhook" "example" { + id = "60f004b957922d2639124f6d" +} + +// Print out the name of the "example" webhook we just recieved from LaunchDarkly +output "launchdarkly_webhook_print" { + value = data.launchdarkly_webhook.example.name + sensitive = false +} diff --git a/examples/v2/environment/example.tf b/examples/v2/environment/example.tf new file mode 100644 index 00000000..4a71c47c --- /dev/null +++ b/examples/v2/environment/example.tf @@ -0,0 +1,21 @@ +// Managing environments directly using the launchdarkly_environment resource is only +// recommended if you intend to manage the project outside of Terraform. If you wish to +// test this configuration, please update the project_key to match an existing project in +// your LaunchDarkly account. See the documentation for more information. +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } + required_version = ">= 0.13" +} + +resource "launchdarkly_environment" "env_test" { + key = "testing-bug" + name = "testing bug" + color = "AAAAAA" + project_key = "default" + confirm_changes = "false" +} diff --git a/examples/v2/feature_flags/README.md b/examples/v2/feature_flags/README.md new file mode 100644 index 00000000..d11156c9 --- /dev/null +++ b/examples/v2/feature_flags/README.md @@ -0,0 +1,301 @@ +## Example: feature flags + +### Introduction + +The LaunchDarkly provider provides two resources for configuring feature flags: [`launchdarkly_feature_flag`](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag.html), which allows you to configure and manipulate project-wide feature flag settings and [`launchdarkly_feature_flag_environment`](https://www.terraform.io/docs/providers/launchdarkly/r/feature_flag_environment.html), which allows you to manage environment-specific feature flag settings, such as [targeting rules](https://docs.launchdarkly.com/home/managing-flags/targeting-users) and [prerequisites](https://docs.launchdarkly.com/home/managing-flags/flag-prerequisites). + +This example contains three config files: + +- [setup.tf](./setup.tf), which auths the provider and creates a project under which the flags will be created +- [flag_types_example.tf](./flag_types_example.tf), which provides examples of the different ways you can define binary (boolean) and multivariate (string, numeric, and JSON) flag variations using the `launchdarkly_feature_flag` resource +- [targeting_example.tf](./targeting_example.tf), which provides complex examples of user targeting using the `launchdarkly_feature_flag_environment` resource. For more detail on user targeting, see the [official LaunchDarkly documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users). + +### Run + +Init your working directory from the CL with `terraform init` and then apply the changes with `terraform apply`. You should see output resembling the following: + +``` +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # launchdarkly_feature_flag.boolean_flag will be created + + resource "launchdarkly_feature_flag" "boolean_flag" { + + description = "An example boolean feature flag that can be turned either on or off" + + id = (known after apply) + + include_in_snippet = false + + key = "boolean-flag" + + maintainer_id = (known after apply) + + name = "Bool feature flag" + + project_key = "tf-flag-examples" + + temporary = false + + variation_type = "boolean" + + + defaults { + + off_variation = (known after apply) + + on_variation = (known after apply) + } + + + variations { + + description = (known after apply) + + name = (known after apply) + + value = (known after apply) + } + } + + # launchdarkly_feature_flag.json_flag will be created + + resource "launchdarkly_feature_flag" "json_flag" { + + description = "An example of a multivariate feature flag with JSON variations" + + id = (known after apply) + + include_in_snippet = false + + key = "json-flag" + + maintainer_id = (known after apply) + + name = "JSON-based feature flag" + + project_key = "tf-flag-examples" + + tags = [ + + "terraform-managed", + ] + + temporary = false + + variation_type = "json" + + + defaults { + + off_variation = (known after apply) + + on_variation = (known after apply) + } + + + variations { + + value = jsonencode( + { + + foo = "bar" + } + ) + } + + variations { + + value = jsonencode( + { + + extra = { + + nested = "json" + } + + foo = "baz" + } + ) + } + } + + # launchdarkly_feature_flag.number_flag will be created + + resource "launchdarkly_feature_flag" "number_flag" { + + description = "An example of a multivariate feature flag with numeric variations" + + id = (known after apply) + + include_in_snippet = false + + key = "number-flag" + + maintainer_id = (known after apply) + + name = "Number value-based feature flag" + + project_key = "tf-flag-examples" + + tags = [ + + "terraform-managed", + ] + + temporary = false + + variation_type = "number" + + + defaults { + + off_variation = (known after apply) + + on_variation = (known after apply) + } + + + variations { + + name = "Big Number Variation" + + value = "123000000" + } + + variations { + + name = "Small Number Variation" + + value = "100" + } + + variations { + + name = "Float Variation" + + value = "123.45" + } + } + + # launchdarkly_feature_flag.string_flag will be created + + resource "launchdarkly_feature_flag" "string_flag" { + + description = "An example of a multivariate feature flag with string variations" + + id = (known after apply) + + include_in_snippet = false + + key = "string-flag" + + maintainer_id = (known after apply) + + name = "String-based feature flag" + + project_key = "tf-flag-examples" + + tags = [ + + "terraform-managed", + ] + + temporary = false + + variation_type = "string" + + + defaults { + + off_variation = (known after apply) + + on_variation = (known after apply) + } + + + variations { + + description = "one of three variations" + + name = "A String" + + value = "string1" + } + + variations { + + name = "Another String" + + value = "string2" + } + + variations { + + name = "Yet Another String" + + value = "string3" + } + } + + # launchdarkly_feature_flag_environment.prereq_flag will be created + + resource "launchdarkly_feature_flag_environment" "prereq_flag" { + + env_key = "production" + + flag_id = (known after apply) + + id = (known after apply) + + off_variation = 1 + + on = true + + track_events = false + + + fallthrough { + + variation = 0 + } + + + prerequisites { + + flag_key = "boolean-flag" + + variation = 1 + } + + + rules { + + clauses { + + attribute = "country" + + negate = false + + op = "matches" + + value_type = "string" + + values = [ + + "uk", + + "aus", + + "usa", + ] + } + } + } + + # launchdarkly_feature_flag_environment.user_targeting_flag will be created + + resource "launchdarkly_feature_flag_environment" "user_targeting_flag" { + + env_key = "test" + + flag_id = (known after apply) + + id = (known after apply) + + off_variation = 0 + + on = true + + track_events = true + + + fallthrough { + + bucket_by = "company" + + rollout_weights = [ + + 60000, + + 30000, + + 10000, + ] + } + + + rules { + + variation = 0 + + + clauses { + + attribute = "name" + + negate = false + + op = "startsWith" + + value_type = "string" + + values = [ + + "a", + + "b", + + "c", + + "d", + + "e", + ] + } + } + + + targets { + + values = [ + + "test_user0", + ] + + variation = 0 + } + + targets { + + values = [ + + "test_user1", + + "test_user2", + ] + + variation = 1 + } + + targets { + + values = [ + + "test_user3", + ] + + variation = 2 + } + } + + # launchdarkly_project.tf_flag_examples will be created + + resource "launchdarkly_project" "tf_flag_examples" { + + id = (known after apply) + + include_in_snippet = false + + key = "tf-flag-examples" + + name = "Terraform Project for Flag Examples" + + tags = [ + + "terraform-managed", + ] + + + environments { + + api_key = (sensitive value) + + client_side_id = (sensitive value) + + color = "ababab" + + confirm_changes = (known after apply) + + default_track_events = (known after apply) + + default_ttl = (known after apply) + + key = "example-env" + + mobile_key = (sensitive value) + + name = "example environment" + + require_comments = (known after apply) + + secure_mode = (known after apply) + } + } + +Plan: 7 to add, 0 to change, 0 to destroy. +``` + +Since Terraform handles all the files in a given directory as a single configuration, all the configurations from the three files in this directory will be applied together. Once you have confirmed the changes to Terraform's prompt, it will apply them with output resembling the following: + +``` +launchdarkly_project.tf_flag_examples: Creating... +launchdarkly_project.tf_flag_examples: Creation complete after 0s [id=tf-flag-examples] +launchdarkly_feature_flag.boolean_flag: Creating... +launchdarkly_feature_flag.json_flag: Creating... +launchdarkly_feature_flag.number_flag: Creating... +launchdarkly_feature_flag.string_flag: Creating... +launchdarkly_feature_flag.boolean_flag: Creation complete after 1s [id=tf-flag-examples/boolean-flag] +launchdarkly_feature_flag.json_flag: Creation complete after 1s [id=tf-flag-examples/json-flag] +launchdarkly_feature_flag.string_flag: Creation complete after 1s [id=tf-flag-examples/string-flag] +launchdarkly_feature_flag.number_flag: Creation complete after 1s [id=tf-flag-examples/number-flag] +launchdarkly_feature_flag_environment.user_targeting_flag: Creating... +launchdarkly_feature_flag_environment.prereq_flag: Creating... +launchdarkly_feature_flag_environment.user_targeting_flag: Creation complete after 0s [id=tf-flag-examples/test/string-flag] +launchdarkly_feature_flag_environment.prereq_flag: Creation complete after 0s [id=tf-flag-examples/production/number-flag] + +Apply complete! Resources: 7 added, 0 changed, 0 destroyed. +``` + +To view your flags, navigate to the Feature flags section on the left sidebar and search for the "terraform-managed" tag: + +!["terraform-managed" tags](../assets/images/feature-flags-variation-types.png) + +You should be able to view specific flag policies by clicking into them. Here you can see how the policies for the user_targeting_flag would look: + +![user_targeting_flag policies](../assets/images/feature-flag-targeting.png) diff --git a/examples/v2/feature_flags/flag_types_example.tf b/examples/v2/feature_flags/flag_types_example.tf new file mode 100644 index 00000000..403118d4 --- /dev/null +++ b/examples/v2/feature_flags/flag_types_example.tf @@ -0,0 +1,97 @@ +# This config provides examples of various flag variation types: boolean, string, numeric, and JSON. + +# ----------------------------------------------------------------------------------- # +# BINARY FLAG +resource "launchdarkly_feature_flag" "boolean_flag" { + project_key = launchdarkly_project.tf_flag_examples.key + key = "boolean-flag" + name = "Bool feature flag" + description = "An example boolean feature flag that can be turned either on or off" + variation_type = "boolean" +} + +# ----------------------------------------------------------------------------------- # +# MULTIVARIATE FLAGS +# For multivariate flags, each variation must be described in a separate 'variations' block +# with required 'value' field and optional 'name' and 'description' fields. + +# create a multivariate flag with string-value variations +resource "launchdarkly_feature_flag" "string_flag" { + project_key = launchdarkly_project.tf_flag_examples.key + key = "string-flag" + name = "String-based feature flag" + description = "An example of a multivariate feature flag with string variations" + + variation_type = "string" + variations { + name = "A String" + description = "one of three variations" + value = "string1" + } + variations { + name = "Another String" + value = "string2" + } + variations { + name = "Yet Another String" + value = "string3" + } + tags = [ + "terraform-managed" + ] +} + +# create a multivariate flag with number-value variations +# Both ints and floats are acceptable, but please note that trailing zeroes +# will be trimmed off of floats, i.e. both 123 and 123.00 will return output 123. +resource "launchdarkly_feature_flag" "number_flag" { + project_key = launchdarkly_project.tf_flag_examples.key + key = "number-flag" + name = "Number value-based feature flag" + description = "An example of a multivariate feature flag with numeric variations" + + variation_type = "number" + variations { + name = "Big Number Variation" + value = 123000000 + } + variations { + name = "Small Number Variation" + value = 100 + } + variations { + name = "Float Variation" + value = 123.45 + } + tags = [ + "terraform-managed" + ] +} + +# create a multivariate flag with JSON-value variations +# Please note that since terraform evaluates all input as strings, +# multi-line input such as jsons must use a marker like "<= len(schemaVariations) { + return nil, fmt.Errorf("default on_variation %v is out of range, must be between 0 and %v inclusive", on, len(schemaVariations)-1) } - if !offFound { - return fmt.Errorf("default_off_variation %q is not defined as a variation", offValue) - } - return nil -} - -func defaultVariationsFromResourceData(d *schema.ResourceData) (*ldapi.Defaults, error) { - err := validateDefaultVariations(d) - if err != nil { - if err == errDefaultsNotSet { - return nil, nil - } - return nil, err - } - schemaVariations := d.Get(VARIATIONS).([]interface{}) - if len(schemaVariations) == 0 { - schemaVariations = defaultBooleanVariations - } - onValue := d.Get(DEFAULT_ON_VARIATION).(string) - offValue := d.Get(DEFAULT_OFF_VARIATION).(string) - - var on *int - var off *int - for i, v := range schemaVariations { - i := i - variation := v.(map[string]interface{}) - val := variation[VALUE].(string) - if val == onValue { - on = &i - } - if val == offValue { - off = &i - } + if off >= len(schemaVariations) { + return nil, fmt.Errorf("default off_variation %v is out of range, must be between 0 and %v inclusive", off, len(schemaVariations)-1) } - return &ldapi.Defaults{OnVariation: int32(*on), OffVariation: int32(*off)}, nil + return &ldapi.Defaults{OnVariation: int32(on), OffVariation: int32(off)}, nil } diff --git a/launchdarkly/default_variations_helper_test.go b/launchdarkly/default_variations_helper_test.go index 5860ba89..4ea95467 100644 --- a/launchdarkly/default_variations_helper_test.go +++ b/launchdarkly/default_variations_helper_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,9 +18,8 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { expectedErr error }{ { - name: "no defaults", + name: "no defaults on boolean", vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", VARIATIONS: []interface{}{ map[string]interface{}{ VALUE: "true", @@ -30,33 +29,14 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { }, }, }, - expected: nil, - }, - { - name: "basic defaults, implicit variations", - vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", - DEFAULT_ON_VARIATION: "true", - DEFAULT_OFF_VARIATION: "false", - }, expected: &ldapi.Defaults{ OnVariation: 0, OffVariation: 1, }, }, - { - name: "invalid defaults, implicit variations", - vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", - DEFAULT_ON_VARIATION: "a", - DEFAULT_OFF_VARIATION: "c", - }, - expectedErr: errors.New(`default_on_variation "a" is not defined as a variation`), - }, { name: "basic defaults", vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", VARIATIONS: []interface{}{ map[string]interface{}{ VALUE: "true", @@ -65,8 +45,11 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { VALUE: "false", }, }, - DEFAULT_ON_VARIATION: "true", - DEFAULT_OFF_VARIATION: "false", + DEFAULTS: []interface{}{ + map[string]interface{}{ + ON_VARIATION: 0, + OFF_VARIATION: 1, + }}, }, expected: &ldapi.Defaults{ OnVariation: 0, @@ -85,46 +68,16 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { VALUE: "false", }, }, - DEFAULT_ON_VARIATION: "not a boolean", - DEFAULT_OFF_VARIATION: "false", - }, - expectedErr: errors.New(`default_on_variation "not a boolean" is not defined as a variation`), - }, - { - name: "invalid default off value", - vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", - VARIATIONS: []interface{}{ - map[string]interface{}{ - VALUE: "true", - }, + DEFAULTS: []interface{}{ map[string]interface{}{ - VALUE: "false", - }, - }, - DEFAULT_ON_VARIATION: "true", - DEFAULT_OFF_VARIATION: "not a boolean", - }, - expectedErr: errors.New(`default_off_variation "not a boolean" is not defined as a variation`), - }, - { - name: "missing default off", - vars: map[string]interface{}{ - VARIATION_TYPE: "boolean", - VARIATIONS: []interface{}{ - map[string]interface{}{ - VALUE: "true", - }, - map[string]interface{}{ - VALUE: "false", - }, - }, - DEFAULT_ON_VARIATION: "true", + ON_VARIATION: 2, + OFF_VARIATION: 1, + }}, }, - expectedErr: errors.New(`default_off_variation is required when default_on_variation is defined`), + expectedErr: errors.New(`default on_variation 2 is out of range, must be between 0 and 1 inclusive`), }, { - name: "missing default on", + name: "invalid default off value", vars: map[string]interface{}{ VARIATION_TYPE: "boolean", VARIATIONS: []interface{}{ @@ -135,33 +88,13 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { VALUE: "false", }, }, - DEFAULT_OFF_VARIATION: "false", - }, - expectedErr: errors.New(`default_on_variation is required when default_off_variation is defined`), - }, - { - name: "string variations", - vars: map[string]interface{}{ - VARIATION_TYPE: "string", - VARIATIONS: []interface{}{ + DEFAULTS: []interface{}{ map[string]interface{}{ - NAME: "nameValue", - DESCRIPTION: "descValue", - VALUE: "a string value", - }, - map[string]interface{}{ - NAME: "nameValue2", - DESCRIPTION: "descValue2", - VALUE: "another string value", - }, - }, - DEFAULT_ON_VARIATION: "a string value", - DEFAULT_OFF_VARIATION: "a string value", - }, - expected: &ldapi.Defaults{ - OnVariation: 0, - OffVariation: 0, + ON_VARIATION: 0, + OFF_VARIATION: 5, + }}, }, + expectedErr: errors.New(`default off_variation 5 is out of range, must be between 0 and 1 inclusive`), }, } @@ -169,13 +102,21 @@ func TestDefaultVariationsFromResourceData(t *testing.T) { t.Run(tc.name, func(t *testing.T) { resourceData := schema.TestResourceDataRaw(t, map[string]*schema.Schema{VARIATION_TYPE: variationTypeSchema(), VARIATIONS: variationsSchema(), - DEFAULT_ON_VARIATION: { - Type: schema.TypeString, - Optional: true, - }, - DEFAULT_OFF_VARIATION: { - Type: schema.TypeString, + DEFAULTS: { + Type: schema.TypeList, Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + ON_VARIATION: { + Type: schema.TypeInt, + Required: true, + }, + OFF_VARIATION: { + Type: schema.TypeInt, + Required: true, + }, + }, + }, }}, tc.vars, ) diff --git a/launchdarkly/destination_helper.go b/launchdarkly/destination_helper.go index b221641d..525a040a 100644 --- a/launchdarkly/destination_helper.go +++ b/launchdarkly/destination_helper.go @@ -3,7 +3,7 @@ package launchdarkly import ( "fmt" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var ( diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index 24399d0a..ffb10871 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -5,8 +5,8 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -41,31 +41,31 @@ func baseEnvironmentSchema(forProject bool) map[string]*schema.Schema { DEFAULT_TTL: { Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 0, // Default TTL should be between 0 and 60 minutes: https://docs.launchdarkly.com/docs/environments Description: "The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK", ValidateFunc: validation.IntBetween(0, 60), }, SECURE_MODE: { - Computed: true, + Default: false, Type: schema.TypeBool, Optional: true, Description: "Whether or not to use secure mode. Secure mode ensures a user of the client-side SDK cannot impersonate another user", }, DEFAULT_TRACK_EVENTS: { - Computed: true, + Default: false, Type: schema.TypeBool, Optional: true, Description: "Whether or not to default to sending data export events for flags created in the environment", }, REQUIRE_COMMENTS: { - Computed: true, + Default: false, Type: schema.TypeBool, Optional: true, Description: "Whether or not to require comments for flag and segment changes in this environment", }, CONFIRM_CHANGES: { - Computed: true, + Default: false, Type: schema.TypeBool, Optional: true, Description: "Whether or not to require confirmation for flag and segment changes in this environment", @@ -132,8 +132,8 @@ func environmentSchema(forProject bool) map[string]*schema.Schema { return schemaMap } -func dataSourceEnvironmentSchema(forPoject bool) map[string]*schema.Schema { - schemaMap := baseEnvironmentSchema(forPoject) +func dataSourceEnvironmentSchema(forProject bool) map[string]*schema.Schema { + schemaMap := baseEnvironmentSchema(forProject) schemaMap[NAME] = &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -225,7 +225,7 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool env := envRaw.(ldapi.Environment) d.SetId(projectKey + "/" + key) - _ = d.Set(key, env.Key) + _ = d.Set(KEY, env.Key) _ = d.Set(NAME, env.Name) _ = d.Set(API_KEY, env.ApiKey) _ = d.Set(MOBILE_KEY, env.MobileKey) diff --git a/launchdarkly/fallthrough_helper.go b/launchdarkly/fallthrough_helper.go index e48768d4..986f10dc 100644 --- a/launchdarkly/fallthrough_helper.go +++ b/launchdarkly/fallthrough_helper.go @@ -3,18 +3,18 @@ package launchdarkly import ( "errors" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) -func fallthroughSchema() *schema.Schema { +func fallthroughSchema(forDataSource bool) *schema.Schema { return &schema.Schema{ Type: schema.TypeList, - Optional: true, + Required: !forDataSource, + Optional: forDataSource, Description: "Nested block describing the default variation to serve if no prerequisites, user_target, or rules apply. You must specify either variation or rollout_weights", - Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -42,10 +42,6 @@ type fallthroughModel struct { } func validateFallThroughResourceData(f []interface{}) error { - if len(f) == 0 { - return nil - } - if !isPercentRollout(f) { fall := f[0].(map[string]interface{}) if bucketBy, ok := fall[BUCKET_BY]; ok { @@ -68,23 +64,12 @@ func isPercentRollout(fall []interface{}) bool { } func fallthroughFromResourceData(d *schema.ResourceData) (fallthroughModel, error) { - var f []interface{} - fallthroughHasChange := d.HasChange(FALLTHROUGH) - flagFallthroughHasChange := d.HasChange(FLAG_FALLTHROUGH) - if fallthroughHasChange { - f = d.Get(FALLTHROUGH).([]interface{}) - } else if flagFallthroughHasChange { - f = d.Get(FLAG_FALLTHROUGH).([]interface{}) - } + f := d.Get(FALLTHROUGH).([]interface{}) err := validateFallThroughResourceData(f) if err != nil { return fallthroughModel{}, err } - if len(f) == 0 { - return fallthroughModel{Variation: intPtr(0)}, nil - } - fall := f[0].(map[string]interface{}) if isPercentRollout(f) { rollout := fallthroughModel{Rollout: rolloutFromResourceData(fall[ROLLOUT_WEIGHTS])} diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index f32bcef4..8129da65 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -7,27 +7,17 @@ import ( "strings" "github.com/antihax/optional" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) -func baseFeatureFlagEnvironmentSchema() map[string]*schema.Schema { - deprecatedFallthrough := fallthroughSchema() - deprecatedFallthrough.Deprecated = "'flag_fallthrough' is deprecated in favor of 'fallthrough'. This field will be removed in the next major release of the LaunchDarkly provider" - deprecatedFallthrough.ConflictsWith = []string{FALLTHROUGH} - newFallthrough := fallthroughSchema() - newFallthrough.ConflictsWith = []string{FLAG_FALLTHROUGH} - deprecatedTargets := targetsSchema() - deprecatedTargets.Deprecated = "'user_targets' is deprecated in favor of 'targets'. This field will be removed in the next major release of the LaunchDarkly provider" - deprecatedTargets.ConflictsWith = []string{TARGETS} - newTargets := targetsSchema() - newTargets.ConflictsWith = []string{USER_TARGETS} +func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Schema { return map[string]*schema.Schema{ FLAG_ID: { Type: schema.TypeString, Required: true, - Description: "The feature flag's unique id in the format `/`", + Description: "The global feature flag's unique id in the format `/`", ForceNew: true, ValidateFunc: validateFlagID, }, @@ -38,38 +28,27 @@ func baseFeatureFlagEnvironmentSchema() map[string]*schema.Schema { ForceNew: true, ValidateFunc: validateKey(), }, - TARGETING_ENABLED: { - Type: schema.TypeBool, - Optional: true, - Deprecated: "'targeting_enabled' is deprecated in favor of 'on'", - Description: "Whether targeting is enabled", - Computed: true, - ConflictsWith: []string{ON}, - }, ON: { - Type: schema.TypeBool, - Optional: true, - Description: "Whether targeting is enabled", - Computed: true, - ConflictsWith: []string{TARGETING_ENABLED}, + Type: schema.TypeBool, + Optional: true, + Description: "Whether targeting is enabled", + Default: false, }, - USER_TARGETS: deprecatedTargets, - TARGETS: newTargets, - RULES: rulesSchema(), - PREREQUISITES: prerequisitesSchema(), - FLAG_FALLTHROUGH: deprecatedFallthrough, - FALLTHROUGH: newFallthrough, + TARGETS: targetsSchema(), + RULES: rulesSchema(), + PREREQUISITES: prerequisitesSchema(), + FALLTHROUGH: fallthroughSchema(forDataSource), TRACK_EVENTS: { Type: schema.TypeBool, Optional: true, Description: "Whether to send event data back to LaunchDarkly", - Computed: true, + Default: false, }, OFF_VARIATION: { Type: schema.TypeInt, - Optional: true, + Required: !forDataSource, + Optional: forDataSource, Description: "The index of the variation to serve if targeting is disabled", - Computed: true, ValidateFunc: validation.IntAtLeast(0), }, } @@ -116,12 +95,10 @@ func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataS if isDataSource { d.SetId(projectKey + "/" + envKey + "/" + flagKey) } - _ = d.Set(KEY, flag.Key) + _ = d.Set(FLAG_ID, projectKey+"/"+flag.Key) // Computed values are set even if they do not exist on the config - _ = d.Set(TARGETING_ENABLED, environment.On) _ = d.Set(ON, environment.On) - _ = d.Set(OFF_VARIATION, environment.OffVariation) _ = d.Set(TRACK_EVENTS, environment.TrackEvents) _ = d.Set(PREREQUISITES, prerequisitesToResourceData(environment.Prerequisites)) @@ -134,24 +111,19 @@ func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataS return fmt.Errorf("failed to set rules on flag with key %q: %v", flagKey, err) } - // user_targets is deprecated in favor of targets err = d.Set(TARGETS, targetsToResourceData(environment.Targets)) if err != nil { return fmt.Errorf("failed to set targets on flag with key %q: %v", flagKey, err) } - err = d.Set(USER_TARGETS, targetsToResourceData(environment.Targets)) - if err != nil { - return fmt.Errorf("failed to set user_targets on flag with key %q: %v", flagKey, err) - } - // flag_fallthrough is deprecated in favor of fallthrough err = d.Set(FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough_)) if err != nil { - return fmt.Errorf("failed to set fallthrough on flag with key %q: %v", flagKey, err) + return fmt.Errorf("failed to set flag fallthrough on flag with key %q: %v", flagKey, err) } - err = d.Set(FLAG_FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough_)) + + err = d.Set(OFF_VARIATION, environment.OffVariation) if err != nil { - return fmt.Errorf("failed to set flag_fallthrough on flag with key %q: %v", flagKey, err) + return fmt.Errorf("failed to set off_variation on flag with key %q: %v", flagKey, err) } return nil diff --git a/launchdarkly/feature_flags_helper.go b/launchdarkly/feature_flags_helper.go index e2f61582..354f0045 100644 --- a/launchdarkly/feature_flags_helper.go +++ b/launchdarkly/feature_flags_helper.go @@ -6,7 +6,8 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -29,7 +30,8 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { MAINTAINER_ID: { Type: schema.TypeString, Optional: true, - Description: "The LaunchDarkly id of the user who will maintain the flag", + Computed: true, + Description: "The LaunchDarkly id of the user who will maintain the flag. If not set, the API will automatically apply the member associated with your Terraform API key or the most recently set maintainer", ValidateFunc: validateID(), }, DESCRIPTION: { @@ -52,15 +54,28 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { }, TAGS: tagsSchema(), CUSTOM_PROPERTIES: customPropertiesSchema(), - DEFAULT_ON_VARIATION: { - Type: schema.TypeString, + DEFAULTS: { + Type: schema.TypeList, Optional: true, - Description: "The value of the variation served when the flag is on for new environments", - }, - DEFAULT_OFF_VARIATION: { - Type: schema.TypeString, - Optional: true, - Description: "The value of the variation served when the flag is off for new environments", + Computed: true, + Description: "The default variations used for this flag in new environments. If omitted, the first and last variation will be used", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + ON_VARIATION: { + Type: schema.TypeInt, + Required: true, + Description: "The index of the variation served when the flag is on for new environments", + ValidateFunc: validation.IntAtLeast(0), + }, + OFF_VARIATION: { + Type: schema.TypeInt, + Required: true, + Description: "The index of the variation served when the flag is off for new environments", + ValidateFunc: validation.IntAtLeast(0), + }, + }, + }, }, } } @@ -85,7 +100,7 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) } transformedCustomProperties := customPropertiesToResourceData(flag.CustomProperties) - _ = d.Set(key, flag.Key) + _ = d.Set(KEY, flag.Key) _ = d.Set(NAME, flag.Name) _ = d.Set(DESCRIPTION, flag.Description) _ = d.Set(INCLUDE_IN_SNIPPET, flag.IncludeInSnippet) @@ -93,10 +108,10 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) if isDataSource { CSA := *flag.ClientSideAvailability - clientSideAvailability := map[string]string{ - "using_environment_id": fmt.Sprintf("%v", CSA.UsingEnvironmentId), - "using_mobile_key": fmt.Sprintf("%v", CSA.UsingMobileKey), - } + clientSideAvailability := []map[string]interface{}{{ + "using_environment_id": CSA.UsingEnvironmentId, + "using_mobile_key": CSA.UsingMobileKey, + }} _ = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) } else { _ = d.Set(INCLUDE_IN_SNIPPET, flag.IncludeInSnippet) @@ -136,23 +151,19 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) return fmt.Errorf("failed to set custom properties on flag with key %q: %v", flag.Key, err) } + var defaults []map[string]interface{} if flag.Defaults != nil { - // this is to prevent the dangling plan values on subsequent post-removal applies - _, ok := d.GetOk(DEFAULT_ON_VARIATION) - // you cannot define one without the other so this should suffice - if ok { - onValue, err := variationValueToString(flag.Variations[flag.Defaults.OnVariation].Value, variationType) - if err != nil { - return err - } - _ = d.Set(DEFAULT_ON_VARIATION, onValue) - offValue, err := variationValueToString(flag.Variations[flag.Defaults.OffVariation].Value, variationType) - if err != nil { - return err - } - _ = d.Set(DEFAULT_OFF_VARIATION, offValue) - } + defaults = []map[string]interface{}{{ + ON_VARIATION: flag.Defaults.OnVariation, + OFF_VARIATION: flag.Defaults.OffVariation, + }} + } else { + defaults = []map[string]interface{}{{ + ON_VARIATION: 0, + OFF_VARIATION: len(flag.Variations) - 1, + }} } + _ = d.Set(DEFAULTS, defaults) d.SetId(projectKey + "/" + key) return nil diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 7e977fd2..0429b811 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -59,19 +59,18 @@ const ( BUCKET_BY = "bucket_by" ROLLOUT_WEIGHTS = "rollout_weights" VARIATION = "variation" - USER_TARGETS = "user_targets" // deprecated TARGETS = "targets" PREREQUISITES = "prerequisites" FLAG_KEY = "flag_key" - TARGETING_ENABLED = "targeting_enabled" TRACK_EVENTS = "track_events" - OFF_VARIATION = "off_variation" - FLAG_FALLTHROUGH = "flag_fallthrough" // deprecated FALLTHROUGH = "fallthrough" KIND = "kind" CONFIG = "config" DEFAULT_ON_VARIATION = "default_on_variation" DEFAULT_OFF_VARIATION = "default_off_variation" + DEFAULTS = "defaults" + ON_VARIATION = "on_variation" + OFF_VARIATION = "off_variation" SERVICE_TOKEN = "service_token" DEFAULT_API_VERSION = "default_api_version" TOKEN = "token" diff --git a/launchdarkly/policies_helper.go b/launchdarkly/policies_helper.go index 72d23c08..6cb1b606 100644 --- a/launchdarkly/policies_helper.go +++ b/launchdarkly/policies_helper.go @@ -4,8 +4,7 @@ import ( "fmt" "sort" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -87,5 +86,5 @@ func policiesToResourceData(policies []ldapi.Policy) interface{} { // https://godoc.org/github.com/hashicorp/terraform/helper/schema#SchemaSetFunc func policyHash(val interface{}) int { policy := policyFromResourceData(val) - return hashcode.String(fmt.Sprintf("%v", policy)) + return schema.HashString(fmt.Sprintf("%v", policy)) } diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 1c509fd0..ad3df49e 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -3,8 +3,8 @@ package launchdarkly import ( "errors" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/policy_statements_helper_test.go b/launchdarkly/policy_statements_helper_test.go index 501a0bf0..a8404606 100644 --- a/launchdarkly/policy_statements_helper_test.go +++ b/launchdarkly/policy_statements_helper_test.go @@ -3,7 +3,7 @@ package launchdarkly import ( "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/launchdarkly/prerequisite_helper.go b/launchdarkly/prerequisite_helper.go index cda3e71b..812f4c0c 100644 --- a/launchdarkly/prerequisite_helper.go +++ b/launchdarkly/prerequisite_helper.go @@ -3,8 +3,8 @@ package launchdarkly import ( "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -13,7 +13,6 @@ func prerequisitesSchema() *schema.Schema { Type: schema.TypeList, Optional: true, Description: "List of nested blocks describing prerequisite feature flags rules", - Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ FLAG_KEY: { diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index 53dacafd..439d6678 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -62,10 +62,10 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er } if isDataSource { defaultCSA := *project.DefaultClientSideAvailability - clientSideAvailability := map[string]string{ - "using_environment_id": fmt.Sprintf("%v", defaultCSA.UsingEnvironmentId), - "using_mobile_key": fmt.Sprintf("%v", defaultCSA.UsingMobileKey), - } + clientSideAvailability := []map[string]interface{}{{ + "using_environment_id": defaultCSA.UsingEnvironmentId, + "using_mobile_key": defaultCSA.UsingMobileKey, + }} err = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) if err != nil { return fmt.Errorf("could not set client_side_availability on project with key %q: %v", project.Key, err) diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 77d01fdb..4041863b 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -3,8 +3,7 @@ package launchdarkly import ( "fmt" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // Environment Variables @@ -21,8 +20,8 @@ const ( api_host = "api_host" ) -// Provider returns a terraform.ResourceProvider. -func Provider() terraform.ResourceProvider { +// Provider returns a *schema.Provider. +func Provider() *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ access_token: { diff --git a/launchdarkly/provider_test.go b/launchdarkly/provider_test.go index 49f0b68f..53de189b 100644 --- a/launchdarkly/provider_test.go +++ b/launchdarkly/provider_test.go @@ -1,23 +1,20 @@ package launchdarkly import ( - "fmt" "os" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // This map is most commonly constructed once in a common init() method of the Providerā€™s main test file, // and includes an object of the current Provider type. https://www.terraform.io/docs/extend/testing/acceptance-tests/testcase.html -var testAccProviders map[string]terraform.ResourceProvider +var testAccProviders map[string]*schema.Provider var testAccProvider *schema.Provider func init() { - testAccProvider = Provider().(*schema.Provider) - testAccProviders = map[string]terraform.ResourceProvider{ + testAccProvider = Provider() + testAccProviders = map[string]*schema.Provider{ "launchdarkly": testAccProvider, } } @@ -27,9 +24,3 @@ func testAccPreCheck(t *testing.T) { t.Fatalf("%s env var must be set for acceptance tests", LAUNCHDARKLY_ACCESS_TOKEN) } } - -// Tags are a TypeSet. TF represents this a as a map of hashes to actual values. -// The hashes are always the same for a given value so this is repeatable. -func testAccTagKey(val string) string { - return fmt.Sprintf("tags.%d", hashcode.String(val)) -} diff --git a/launchdarkly/resource_launchdarkly_access_token.go b/launchdarkly/resource_launchdarkly_access_token.go index 915f1a7b..156bbfbc 100644 --- a/launchdarkly/resource_launchdarkly_access_token.go +++ b/launchdarkly/resource_launchdarkly_access_token.go @@ -6,8 +6,8 @@ import ( "net/http" "github.com/antihax/optional" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/resource_launchdarkly_access_token_test.go b/launchdarkly/resource_launchdarkly_access_token_test.go index 013a7508..76744ce7 100644 --- a/launchdarkly/resource_launchdarkly_access_token_test.go +++ b/launchdarkly/resource_launchdarkly_access_token_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( @@ -296,7 +296,7 @@ func TestAccAccessToken_UpdateCustomRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(name), name), + resource.TestCheckResourceAttr(resourceName, "custom_roles.0", name), ), }, { @@ -305,8 +305,8 @@ func TestAccAccessToken_UpdateCustomRole(t *testing.T) { testAccCheckAccessTokenExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "Updated - "+name), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "2"), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(name), name), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(name+"2"), name+"2"), + resource.TestCheckResourceAttr(resourceName, "custom_roles.0", name), + resource.TestCheckResourceAttr(resourceName, "custom_roles.1", name+"2"), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_custom_role.go b/launchdarkly/resource_launchdarkly_custom_role.go index c8f2319f..4b32f21b 100644 --- a/launchdarkly/resource_launchdarkly_custom_role.go +++ b/launchdarkly/resource_launchdarkly_custom_role.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/resource_launchdarkly_custom_role_test.go b/launchdarkly/resource_launchdarkly_custom_role_test.go index 01d3ce3b..f5b073d7 100644 --- a/launchdarkly/resource_launchdarkly_custom_role_test.go +++ b/launchdarkly/resource_launchdarkly_custom_role_test.go @@ -4,11 +4,9 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" - ldapi "github.com/launchdarkly/api-client-go" + "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 ( @@ -65,11 +63,6 @@ func TestAccCustomRole_Create(t *testing.T) { key := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_custom_role.test" - policy := ldapi.Policy{ - Resources: []string{"proj/*:env/production"}, - Actions: []string{"*"}, - Effect: "deny", - } resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) @@ -84,11 +77,11 @@ func TestAccCustomRole_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "Custom role - "+name), resource.TestCheckResourceAttr(resourceName, "description", "Deny all actions on production environments"), resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "actions.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "actions.0"), "*"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "resources.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "resources.0"), "proj/*:env/production"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, EFFECT), "deny"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.0", "proj/*:env/production"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "deny"), ), }, }, @@ -134,11 +127,6 @@ func TestAccCustomRole_Update(t *testing.T) { key := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_custom_role.test" - policy := ldapi.Policy{ - Resources: []string{"proj/*:env/staging"}, - Actions: []string{"*"}, - Effect: "allow", - } resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) @@ -159,11 +147,11 @@ func TestAccCustomRole_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "Updated - "+name), resource.TestCheckResourceAttr(resourceName, "description", ""), // should be empty after removal resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "actions.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "actions.0"), "*"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "resources.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, "resources.0"), "proj/*:env/staging"), - resource.TestCheckResourceAttr(resourceName, testAccPolicyKey(policy, EFFECT), "allow"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.0", "proj/*:env/staging"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "allow"), ), }, }, @@ -205,9 +193,6 @@ func TestAccCustomRole_UpdateWithStatements(t *testing.T) { }, }) } -func testAccPolicyKey(policy ldapi.Policy, subkey string) string { - return fmt.Sprintf("policy.%d.%s", hashcode.String(fmt.Sprintf("%v", policy)), subkey) -} func testAccCheckCustomRoleExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { diff --git a/launchdarkly/resource_launchdarkly_destination.go b/launchdarkly/resource_launchdarkly_destination.go index cb931363..f904aa2d 100644 --- a/launchdarkly/resource_launchdarkly_destination.go +++ b/launchdarkly/resource_launchdarkly_destination.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -56,18 +56,10 @@ func resourceDestination() *schema.Resource { Description: "The destination-specific configuration object corresponding to your data export kind - see documentation for required fields for each kind", Elem: &schema.Schema{Type: schema.TypeString}, }, - ENABLED: { - Type: schema.TypeBool, - Description: "Whether the data export destination is on or not. This field has been deprecated in favor of 'on'", - Deprecated: "'enabled' is deprecated in favor of 'on'", - Optional: true, - ConflictsWith: []string{ON}, - }, ON: { - Type: schema.TypeBool, - Description: "Whether the data export destination is on or not", - Optional: true, - ConflictsWith: []string{ENABLED}, + Type: schema.TypeBool, + Description: "Whether the data export destination is on or not", + Optional: true, }, TAGS: tagsSchema(), }, @@ -80,7 +72,7 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro destinationEnvKey := d.Get(ENV_KEY).(string) destinationName := d.Get(NAME).(string) destinationKind := d.Get(KIND).(string) - destinationOn := getDestinationOn(d) + destinationOn := d.Get(ON).(bool) destinationConfig, err := destinationConfigFromResourceData(d) if err != nil { @@ -138,11 +130,7 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error _ = d.Set(NAME, destination.Name) _ = d.Set(KIND, destination.Kind) _ = d.Set(CONFIG, preservedCfg) - if _, ok := d.GetOkExists(ENABLED); ok { - d.Set(ENABLED, destination.On) - } else { - d.Set(ON, destination.On) - } + _ = d.Set(ON, destination.On) d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, destination.Id}, "/")) return nil @@ -162,7 +150,7 @@ func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) erro if err != nil { return err } - destinationOn := getDestinationOn(d) + destinationOn := d.Get(ON).(bool) patch := []ldapi.PatchOperation{ patchReplace("/name", &destinationName), @@ -246,18 +234,3 @@ func destinationImportIDtoKeys(importID string) (projKey, envKey, destinationID projKey, envKey, destinationID = parts[0], parts[1], parts[2] return projKey, envKey, destinationID, nil } - -// getDestinationOn is a helper function used for deprecating ENABLED in favor of ON to match -// LD's API response. It will default to false if neither is set and we will overwrite the existing -// value with false if it is removed. -func getDestinationOn(d *schema.ResourceData) bool { - var destinationOn bool - on, onSet := d.GetOkExists(ON) - enabled, enabledSet := d.GetOkExists(ENABLED) - if onSet { - destinationOn = on.(bool) - } else if enabledSet { - destinationOn = enabled.(bool) - } - return destinationOn -} diff --git a/launchdarkly/resource_launchdarkly_destination_test.go b/launchdarkly/resource_launchdarkly_destination_test.go index e487871d..97309f42 100644 --- a/launchdarkly/resource_launchdarkly_destination_test.go +++ b/launchdarkly/resource_launchdarkly_destination_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( @@ -21,7 +21,7 @@ resource "launchdarkly_destination" "test" { role_arn = "arn:aws:iam::123456789012:role/marketingadmin" stream_name = "cat-stream" } - enabled = true + on = true tags = [ "terraform" ] } ` @@ -35,7 +35,6 @@ resource "launchdarkly_destination" "test" { project = "test-project" topic = "test-topic" } - enabled = true tags = [ "terraform" ] } ` @@ -51,7 +50,7 @@ resource "launchdarkly_destination" "test" { user_identity = "customer_id" environment = "production" } - enabled = true + on = true tags = [ "terraform" ] } ` @@ -65,7 +64,7 @@ resource "launchdarkly_destination" "test" { config = { write_key = "super-secret-write-key" } - enabled = true + on = true tags = [ "terraform" ] } ` @@ -98,7 +97,7 @@ resource "launchdarkly_destination" "test" { role_arn = "arn:aws:iam::123456789012:role/marketingadmin", stream_name = "cat-stream" } - enabled = true + on = true tags = [ "terraform", "updated" ] } ` @@ -112,7 +111,7 @@ resource "launchdarkly_destination" "test" { "project": "renamed-project", "topic": "test-topic" } - enabled = true + on = true tags = [ "terraform", "updated" ] } ` @@ -129,7 +128,7 @@ resource "launchdarkly_destination" "test" { user_identity = "customer_id" environment = "production" } - enabled = true + on = true tags = [ "terraform", "updated" ] } ` @@ -143,7 +142,6 @@ resource "launchdarkly_destination" "test" { config = { write_key = "updated-write-key" } - enabled = true tags = [ "terraform" ] } ` @@ -187,7 +185,8 @@ func TestAccDestination_CreateKinesis(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "kinesis-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.region", "us-east-1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -217,7 +216,7 @@ func TestAccDestination_CreateMparticle(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "mparticle-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.api_key", "apiKeyfromMParticle"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -241,8 +240,9 @@ func TestAccDestination_CreatePubsub(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "env_key", "test"), resource.TestCheckResourceAttr(resourceName, "name", "pubsub-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), resource.TestCheckResourceAttr(resourceName, "config.project", "test-project"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -267,7 +267,7 @@ func TestAccDestination_CreateSegment(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "segment"), resource.TestCheckResourceAttr(resourceName, "config.write_key", "super-secret-write-key"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -295,7 +295,7 @@ func TestAccDestination_CreateAzureEventHubs(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "config.name", "name"), resource.TestCheckResourceAttr(resourceName, "config.policy_name", "policy-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_key", "super-secret-policy-key"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -320,7 +320,7 @@ func TestAccDestination_UpdateKinesis(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "kinesis-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.role_arn", "arn:aws:iam::123456789012:role/marketingadmin"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -331,8 +331,8 @@ func TestAccDestination_UpdateKinesis(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "updated-kinesis-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.role_arn", "arn:aws:iam::123456789012:role/marketingadmin"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("updated"), "updated"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -357,7 +357,7 @@ func TestAccDestination_UpdatePubsub(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "pubsub-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), resource.TestCheckResourceAttr(resourceName, "config.project", "test-project"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -368,8 +368,8 @@ func TestAccDestination_UpdatePubsub(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "updated-pubsub-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), resource.TestCheckResourceAttr(resourceName, "config.project", "renamed-project"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("updated"), "updated"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -395,7 +395,7 @@ func TestAccDestination_UpdateMparticle(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.secret", "mParticleSecret"), resource.TestCheckResourceAttr(resourceName, "config.environment", "production"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -407,8 +407,8 @@ func TestAccDestination_UpdateMparticle(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.secret", "updatedSecret"), resource.TestCheckResourceAttr(resourceName, "config.environment", "production"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("updated"), "updated"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), ), }, }, @@ -432,8 +432,9 @@ func TestAccDestination_UpdateSegment(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "env_key", "test"), resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "segment"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "config.write_key", "super-secret-write-key"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -444,8 +445,9 @@ func TestAccDestination_UpdateSegment(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "env_key", "test"), resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), resource.TestCheckResourceAttr(resourceName, "kind", "segment"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), // should default to false when removed resource.TestCheckResourceAttr(resourceName, "config.write_key", "updated-write-key"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, @@ -474,7 +476,7 @@ func TestAccDestination_UpdateAzureEventHubs(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "config.policy_name", "policy-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_key", "super-secret-policy-key"), resource.TestCheckResourceAttr(resourceName, "on", "true"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, { @@ -490,7 +492,7 @@ func TestAccDestination_UpdateAzureEventHubs(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "config.policy_name", "updated-policy-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_key", "updated-policy-key"), resource.TestCheckResourceAttr(resourceName, "on", "false"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_environment.go b/launchdarkly/resource_launchdarkly_environment.go index afe41c0e..d4a0ad2b 100644 --- a/launchdarkly/resource_launchdarkly_environment.go +++ b/launchdarkly/resource_launchdarkly_environment.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/resource_launchdarkly_environment_test.go b/launchdarkly/resource_launchdarkly_environment_test.go index f42a630b..7672c493 100644 --- a/launchdarkly/resource_launchdarkly_environment_test.go +++ b/launchdarkly/resource_launchdarkly_environment_test.go @@ -5,20 +5,20 @@ import ( "regexp" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( testAccEnvironmentCreate = ` resource "launchdarkly_environment" "staging" { name = "Staging1" - key = "staging1" - color = "ff00ff" - secure_mode = true - default_track_events = true - default_ttl = 50 + key = "staging1" + color = "ff00ff" + secure_mode = true + default_track_events = true + default_ttl = 50 project_key = launchdarkly_project.test.key tags = ["tagged", "terraform"] require_comments = true @@ -29,25 +29,34 @@ resource "launchdarkly_environment" "staging" { testAccEnvironmentUpdate = ` resource "launchdarkly_environment" "staging" { name = "The real staging1" - key = "staging1" - color = "000000" - secure_mode = false - default_track_events = false - default_ttl = 3 + key = "staging1" + color = "000000" + secure_mode = false + default_track_events = false + default_ttl = 3 project_key = launchdarkly_project.test.key require_comments = false confirm_changes = false } +` + + testAccEnvironmentRemoveOptionalAttributes = ` +resource "launchdarkly_environment" "staging" { + name = "The real staging1" + key = "staging1" + color = "000000" + project_key = launchdarkly_project.test.key +} ` testAccEnvironmentInvalid = ` resource "launchdarkly_environment" "staging" { name = "The real staging1" - key = "staging1" - color = "000000" - secure_mode = false - default_track_events = "maybe" - default_ttl = 3 + key = "staging1" + color = "000000" + secure_mode = false + default_track_events = "maybe" + default_ttl = 3 project_key = launchdarkly_project.test.key require_comments = false confirm_changes = true @@ -79,8 +88,8 @@ func TestAccEnvironment_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "require_comments", "true"), resource.TestCheckResourceAttr(resourceName, "confirm_changes", "true"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("tagged"), "tagged"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "tagged"), ), }, { @@ -135,6 +144,49 @@ func TestAccEnvironment_Update(t *testing.T) { }) } +func TestAccEnvironment_RemoveAttributes(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_environment.staging" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccEnvironmentCreate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Staging1"), + resource.TestCheckResourceAttr(resourceName, "key", "staging1"), + resource.TestCheckResourceAttr(resourceName, "color", "ff00ff"), + resource.TestCheckResourceAttr(resourceName, "secure_mode", "true"), + resource.TestCheckResourceAttr(resourceName, "default_track_events", "true"), + resource.TestCheckResourceAttr(resourceName, "default_ttl", "50"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + ), + }, + { + Config: withRandomProject(projectKey, testAccEnvironmentRemoveOptionalAttributes), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "The real staging1"), + resource.TestCheckResourceAttr(resourceName, "key", "staging1"), + resource.TestCheckResourceAttr(resourceName, "color", "000000"), + resource.TestCheckResourceAttr(resourceName, "secure_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "default_track_events", "false"), + resource.TestCheckResourceAttr(resourceName, "default_ttl", "0"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "require_comments", "false"), + resource.TestCheckResourceAttr(resourceName, "confirm_changes", "false"), + ), + }, + }, + }) +} + func TestAccEnvironment_Invalid(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_environment.staging" @@ -146,7 +198,7 @@ func TestAccEnvironment_Invalid(t *testing.T) { Steps: []resource.TestStep{ { Config: withRandomProject(projectKey, testAccEnvironmentInvalid), - ExpectError: regexp.MustCompile("config is invalid"), + ExpectError: regexp.MustCompile("Error: Incorrect attribute value type"), // default_track_events should be bool }, { Config: withRandomProject(projectKey, testAccEnvironmentUpdate), diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 7d04518a..5449abc0 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment.go b/launchdarkly/resource_launchdarkly_feature_flag_environment.go index 8ec1ca86..a42d37f1 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -20,7 +20,7 @@ func resourceFeatureFlagEnvironment() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceFeatureFlagEnvironmentImport, }, - Schema: baseFeatureFlagEnvironmentSchema(), + Schema: baseFeatureFlagEnvironmentSchema(false), } } @@ -64,18 +64,12 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf patches := make([]ldapi.PatchOperation, 0) - enabled, ok := getFeatureFlagEnvironmentOn(d) - if ok { - patches = append(patches, patchReplace(patchFlagEnvPath(d, "on"), enabled)) - } + on := d.Get(ON) + patches = append(patches, patchReplace(patchFlagEnvPath(d, "on"), on)) - // GetOKExists is marked deprecated by Hashicorp, however it seems to be the only solution for setting the - // offVariation to 0 during creation. According to Hashicorp, it will not be removed until a replacement function is - // implemented. https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 - offVariation, ok := d.GetOkExists(OFF_VARIATION) - if ok { - patches = append(patches, patchReplace(patchFlagEnvPath(d, "offVariation"), offVariation.(int))) - } + // off_variation is required + offVariation := d.Get(OFF_VARIATION) + patches = append(patches, patchReplace(patchFlagEnvPath(d, "offVariation"), offVariation.(int))) trackEvents, ok := d.GetOk(TRACK_EVENTS) if ok { @@ -97,22 +91,18 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf patches = append(patches, patchReplace(patchFlagEnvPath(d, "prerequisites"), prerequisites)) } - _, oldOk := d.GetOk(USER_TARGETS) - _, newOk := d.GetOk(TARGETS) - if oldOk || newOk { + _, ok = d.GetOk(TARGETS) + if ok { targets := targetsFromResourceData(d) patches = append(patches, patchReplace(patchFlagEnvPath(d, "targets"), targets)) } - _, newOk = d.GetOk(FALLTHROUGH) - _, oldOk = d.GetOk(FLAG_FALLTHROUGH) - if oldOk || newOk { - fall, err := fallthroughFromResourceData(d) - if err != nil { - return err - } - patches = append(patches, patchReplace(patchFlagEnvPath(d, "fallthrough"), fall)) + // fallthrough is required + fall, err := fallthroughFromResourceData(d) + if err != nil { + return err } + patches = append(patches, patchReplace(patchFlagEnvPath(d, "fallthrough"), fall)) if len(patches) > 0 { patch := ldapi.PatchComment{ @@ -162,7 +152,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf return fmt.Errorf("failed to find environment with key %q", envKey) } - on, _ := getFeatureFlagEnvironmentOn(d) + on := d.Get(ON) rules, err := rulesFromResourceData(d) if err != nil { return err @@ -170,12 +160,12 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf trackEvents := d.Get(TRACK_EVENTS).(bool) prerequisites := prerequisitesFromResourceData(d, PREREQUISITES) targets := targetsFromResourceData(d) - offVariation := d.Get(OFF_VARIATION).(int) fall, err := fallthroughFromResourceData(d) if err != nil { return err } + offVariation := d.Get(OFF_VARIATION) patch := ldapi.PatchComment{ Comment: "Terraform", @@ -184,9 +174,9 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf patchReplace(patchFlagEnvPath(d, "rules"), rules), patchReplace(patchFlagEnvPath(d, "trackEvents"), trackEvents), patchReplace(patchFlagEnvPath(d, "prerequisites"), prerequisites), - patchReplace(patchFlagEnvPath(d, "offVariation"), offVariation), patchReplace(patchFlagEnvPath(d, "targets"), targets), patchReplace(patchFlagEnvPath(d, "fallthrough"), fall), + patchReplace(patchFlagEnvPath(d, "offVariation"), offVariation), }} log.Printf("[DEBUG] %+v\n", patch) @@ -270,21 +260,3 @@ func resourceFeatureFlagEnvironmentImport(d *schema.ResourceData, meta interface return []*schema.ResourceData{d}, nil } - -// getFeatureFlagEnvironmentOn is a helper function used for deprecating TARGETING_ENABLED in favor of ON to match -// LD's API response. It returns nil if neither is set so we can maintain current behavior - TODO in V2 it will need -// to be updated to default to false if neither is set. -func getFeatureFlagEnvironmentOn(d *schema.ResourceData) (bool, bool) { - var onValue bool - on, onSet := d.GetOk(ON) - enabled, enabledSet := d.GetOk(TARGETING_ENABLED) - if !onSet && !enabledSet { - return onValue, false - } - if onSet { - onValue = on.(bool) - } else if enabledSet { - onValue = enabled.(bool) - } - return onValue, true -} diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go index b701fc5d..28be97b8 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/antihax/optional" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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" ldapi "github.com/launchdarkly/api-client-go" ) @@ -34,12 +34,14 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "basic" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" - targeting_enabled = false - flag_fallthrough { + on = false + fallthrough { variation = 1 } - user_targets { - values = ["user1"] + off_variation = 2 + targets { + values = ["user1"] + variation = 0 } } ` @@ -64,6 +66,10 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "basic" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" + fallthrough { + variation = 0 + } + off_variation = 2 } ` @@ -87,16 +93,15 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "basic" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" - targeting_enabled = true + on = true track_events = true targets { - values = [] - } - targets { - values = ["user1", "user2"] + values = ["user1", "user2"] + variation = 1 } targets { - values = [] + values = [] + variation = 2 } rules { clauses { @@ -122,42 +127,7 @@ resource "launchdarkly_feature_flag_environment" "basic" { rollout_weights = [60000, 40000, 0] bucket_by = "email" } -} -` - testAccFeatureFlagEnvironmentUpdateDeprecated = ` -resource "launchdarkly_feature_flag" "basic" { - project_key = launchdarkly_project.test.key - key = "basic-flag" - name = "Basic feature flag" - variation_type = "number" - variations { - value = 0 - } - variations { - value = 10 - } - variations { - value = 30 - } -} - -resource "launchdarkly_feature_flag_environment" "basic" { - flag_id = launchdarkly_feature_flag.basic.id - env_key = "test" - targeting_enabled = true - track_events = true - user_targets { - values = ["user1", "user2"] - } - user_targets { - values = [] - } - user_targets { - values = [] - } - flag_fallthrough { - variation = 2 - } + off_variation = 1 } ` @@ -179,9 +149,9 @@ resource "launchdarkly_feature_flag_environment" "json_variations" { flag_id = launchdarkly_feature_flag.json.id env_key = "test" - flag_fallthrough { + fallthrough { variation = 1 - } + } off_variation = 0 } @@ -214,12 +184,49 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "prereq" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" - targeting_enabled = true - + on = true prerequisites { flag_key = launchdarkly_feature_flag.bool.key variation = 0 } + fallthrough { + variation = 1 + } + off_variation = 0 +} +` + + testAccFeatureFlagEnvironmentRemovePrereq = ` +resource "launchdarkly_feature_flag" "bool" { + project_key = launchdarkly_project.test.key + key = "bool-flag" + name = "boolean flag" + variation_type = "boolean" +} + +resource "launchdarkly_feature_flag" "basic" { + project_key = launchdarkly_project.test.key + key = "basic-flag" + name = "Basic feature flag" + variation_type = "number" + variations { + value = 10 + } + variations { + value = 20 + } + variations { + value = 30 + } +} + +resource "launchdarkly_feature_flag_environment" "prereq" { + flag_id = launchdarkly_feature_flag.basic.id + env_key = "test" + fallthrough { + variation = 1 + } + off_variation = 0 } ` @@ -234,8 +241,7 @@ resource "launchdarkly_feature_flag" "bool_flag" { resource "launchdarkly_feature_flag_environment" "bool_clause" { flag_id = launchdarkly_feature_flag.bool_flag.id env_key = "test" - targeting_enabled = true - + on = true rules { clauses { attribute = "is_vip" @@ -246,6 +252,10 @@ resource "launchdarkly_feature_flag_environment" "bool_clause" { } variation = 0 } + fallthrough { + variation = 0 + } + off_variation = 1 } ` @@ -260,8 +270,7 @@ resource "launchdarkly_feature_flag" "bool_flag" { resource "launchdarkly_feature_flag_environment" "number_clause" { flag_id = launchdarkly_feature_flag.bool_flag.id env_key = "test" - targeting_enabled = true - + on = true rules { clauses { attribute = "answer" @@ -272,6 +281,10 @@ resource "launchdarkly_feature_flag_environment" "number_clause" { } variation = 0 } + fallthrough { + variation = 0 + } + off_variation = 1 } ` @@ -295,11 +308,12 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "invalid_bucket_by" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" - targeting_enabled = true + on = true - flag_fallthrough { + fallthrough { bucket_by = "email" } + off_variation = 0 } ` @@ -323,8 +337,7 @@ resource "launchdarkly_feature_flag" "basic" { resource "launchdarkly_feature_flag_environment" "invalid_bucket_by" { flag_id = launchdarkly_feature_flag.basic.id env_key = "test" - targeting_enabled = true - + on = true rules { clauses { attribute = "name" @@ -335,6 +348,10 @@ resource "launchdarkly_feature_flag_environment" "invalid_bucket_by" { variation = 0 bucket_by = "name" } + fallthrough { + variation = 0 + } + off_variation = 1 } ` ) @@ -352,16 +369,19 @@ func TestAccFeatureFlagEnvironment_Basic(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "0"), ), }, { - ResourceName: resourceName, - ImportState: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -380,28 +400,28 @@ func TestAccFeatureFlagEnvironment_Empty(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentEmpty), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "track_events", "false"), resource.TestCheckNoResourceAttr(resourceName, "rules"), resource.TestCheckNoResourceAttr(resourceName, "rules.#"), resource.TestCheckNoResourceAttr(resourceName, "prerequisites"), resource.TestCheckNoResourceAttr(resourceName, "prerequisites.#"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "0"), - resource.TestCheckNoResourceAttr(resourceName, "user_targets"), - resource.TestCheckNoResourceAttr(resourceName, "user_targets.#"), + resource.TestCheckNoResourceAttr(resourceName, "targets"), + resource.TestCheckNoResourceAttr(resourceName, "targets.#"), ), }, { - ResourceName: resourceName, - ImportState: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) } -func TestAccFeatureFlagEnvironment_UpdateDeprecatedFields(t *testing.T) { +func TestAccFeatureFlagEnvironment_Update(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_feature_flag_environment.basic" resource.ParallelTest(t, resource.TestCase{ @@ -414,73 +434,22 @@ func TestAccFeatureFlagEnvironment_UpdateDeprecatedFields(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "on", "false"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "rules.#", "0"), - ), - }, - { - Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdateDeprecated), - Check: resource.ComposeTestCheckFunc( - testAccCheckFeatureFlagEnvironmentExists(resourceName), - // computed values should come through - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), - resource.TestCheckResourceAttr(resourceName, "track_events", "true"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "2"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "2"), - resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "1"), resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "targets.0.values.1", "user2"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.1", "user2"), - resource.TestCheckResourceAttr(resourceName, "user_targets.1.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.2.values.#", "0"), - ), - }, - }, - }) -} - -func TestAccFeatureFlagEnvironment_Update(t *testing.T) { - projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - resourceName := "launchdarkly_feature_flag_environment.basic" - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), - Check: resource.ComposeTestCheckFunc( - testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.rollout.#", "0"), - resource.TestCheckResourceAttr(resourceName, "user_targets.#", "1"), - resource.TestCheckResourceAttr(resourceName, "user_targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "rules.#", "0"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), ), }, { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdate), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "track_events", "true"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), @@ -489,12 +458,13 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.1", "40000"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.2", "0"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.bucket_by", "email"), - resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.1", "user2"), - resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.#", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "targets.0.values.1", "user2"), + resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "0"), + resource.TestCheckResourceAttr(resourceName, "targets.1.variation", "2"), resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), @@ -514,49 +484,27 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.0", "h"), resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.negate", "false"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), ), }, - // After changes have been made to the resource, removing optional values should not change them. + // After changes have been made to the resource, removing optional values should revert to their default / null values. { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentEmpty), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), - resource.TestCheckResourceAttr(resourceName, "track_events", "true"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, "track_events", "false"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.#", "3"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.0", "60000"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.1", "40000"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.2", "0"), - resource.TestCheckResourceAttr(resourceName, "fallthrough.0.bucket_by", "email"), - resource.TestCheckResourceAttr(resourceName, "targets.#", "3"), - resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.0", "user1"), - resource.TestCheckResourceAttr(resourceName, "targets.1.values.1", "user2"), - resource.TestCheckResourceAttr(resourceName, "targets.2.values.#", "0"), - resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), - resource.TestCheckResourceAttr(resourceName, "rules.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", "country"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.op", "startsWith"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.#", "2"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", "great"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.1", "amazing"), - resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.negate", "false"), - resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.#", "3"), - resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.0", "90000"), - resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.1", "10000"), - resource.TestCheckResourceAttr(resourceName, "rules.1.rollout_weights.2", "0"), - resource.TestCheckResourceAttr(resourceName, "rules.1.bucket_by", "email"), - resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.attribute", "name"), - resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.op", "startsWith"), - resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.#", "1"), - resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.0", "h"), - resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.negate", "false"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckNoResourceAttr(resourceName, "targets.#"), + resource.TestCheckNoResourceAttr(resourceName, "rules.#"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -574,16 +522,17 @@ func TestAccFeatureFlagEnvironment_JSON_variations(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentJSONVariations), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.#", "1"), - resource.TestCheckResourceAttr(resourceName, "flag_fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{FALLTHROUGH, OFF_VARIATION}, }, }, }) @@ -602,12 +551,14 @@ func TestAccFeatureFlagEnvironment_BoolClauseValue(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBoolClauseValue), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.value_type", "boolean"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", "true"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), ), }, { @@ -632,13 +583,15 @@ func TestAccFeatureFlagEnvironment_NumberClauseValue(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentNumberClauseValue), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.value_type", "number"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", "42"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.1", "84"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), ), }, { @@ -684,9 +637,21 @@ func TestAccFeatureFlagEnvironment_Prereq(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentPrereq), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "targeting_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "prerequisites.#", "1"), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.flag_key", "bool-flag"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentRemovePrereq), + Check: resource.ComposeTestCheckFunc( + testAccCheckFeatureFlagEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckNoResourceAttr(resourceName, "prerequisites.#"), + resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), + resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index 4ac85cb1..6faaf260 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -5,10 +5,9 @@ import ( "regexp" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( @@ -30,8 +29,10 @@ resource "launchdarkly_feature_flag" "basic" { tags = ["update", "terraform"] include_in_snippet = true temporary = true - default_on_variation = "true" - default_off_variation = "false" + defaults { + on_variation = 0 + off_variation = 1 + } } ` @@ -131,8 +132,27 @@ resource "launchdarkly_feature_flag" "maintained" { } ` - //testAccFeatureFlagWasMaintained is used to test that feature flag maintainers can be unset - testAccFeatureFlagWasMaintained = ` + // if the maintainer id is removed from the config it should still be set in the state to + // the previous maintainer if that maintainer still exists + testAccFeatureFlagMaintainerComputed = ` +resource "launchdarkly_team_member" "test" { + email = "%s@example.com" + first_name = "first" + last_name = "last" + role = "admin" + custom_roles = [] +} + +resource "launchdarkly_feature_flag" "maintained" { + project_key = launchdarkly_project.test.key + key = "maintained-flag" + name = "Maintained feature flag" + variation_type = "boolean" +} +` + + //testAccFeatureFlagMaintainerDeleted is used to test that feature flag maintainers can be unset + testAccFeatureFlagMaintainerDeleted = ` resource "launchdarkly_feature_flag" "maintained" { project_key = launchdarkly_project.test.key key = "maintained-flag" @@ -256,6 +276,10 @@ resource "launchdarkly_feature_flag" "multivariate" { "value3" ] } + defaults { + on_variation = 2 + off_variation = 1 + } } ` @@ -265,8 +289,10 @@ resource "launchdarkly_feature_flag" "defaults" { key = "defaults-flag" name = "Feature flag with defaults" variation_type = "boolean" - default_on_variation = "true" - default_off_variation = "false" + defaults { + on_variation = 0 + off_variation = 1 + } } ` testAccFeatureFlagDefaultsUpdate = ` @@ -275,8 +301,10 @@ resource "launchdarkly_feature_flag" "defaults" { key = "defaults-flag" name = "Feature flag with defaults" variation_type = "boolean" - default_on_variation = "true" - default_off_variation = "true" + defaults { + on_variation = 0 + off_variation = 0 + } } ` testAccFeatureFlagDefaultsMissingOffInvalid = ` @@ -285,8 +313,10 @@ resource "launchdarkly_feature_flag" "defaults" { key = "defaults-flag" name = "Feature flag with defaults" variation_type = "boolean" - default_on_variation = "a" - default_off_variation = "b" + defaults { + on_variation = 2 + off_variation = 3 + } } ` @@ -296,8 +326,10 @@ resource "launchdarkly_feature_flag" "defaults-multivariate" { key = "defaults-multivariate-flag" name = "Multivariate feature flag with defaults" variation_type = "string" - default_on_variation = "b" - default_off_variation = "b" + defaults { + on_variation = 1 + off_variation = 1 + } variations { value = "a" } @@ -318,8 +350,10 @@ resource "launchdarkly_feature_flag" "defaults-multivariate" { key = "defaults-multivariate-flag" name = "Multivariate feature flag with defaults" variation_type = "string" - default_on_variation = "c" - default_off_variation = "c" + defaults { + on_variation = 2 + off_variation = 2 + } variations { value = "a" } @@ -340,8 +374,10 @@ resource "launchdarkly_feature_flag" "defaults-multivariate" { key = "defaults-multivariate-flag" name = "Multivariate fature flag with defaults" variation_type = "string" - default_on_variation = "c" - default_off_variation = "c" + defaults { + on_variation = 2 + off_variation = 2 + } variations { value = "b" } @@ -374,6 +410,11 @@ func withRandomProject(randomProject, resource string) string { resource "launchdarkly_project" "test" { name = "testProject" key = "%s" + environments { + name = "testEnvironment" + key = "test" + color = "000000" + } } %s`, randomProject, resource) @@ -441,12 +482,12 @@ func TestAccFeatureFlag_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), resource.TestCheckResourceAttr(resourceName, "description", "this is a boolean flag by default becausethe variations field is omitted"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("update"), "update"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "update"), resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), resource.TestCheckResourceAttr(resourceName, "temporary", "true"), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "true"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "false"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "0"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), }, }, @@ -562,14 +603,28 @@ func TestAccFeatureFlag_WithMaintainer(t *testing.T) { ), }, { - Config: withRandomProject(projectKey, testAccFeatureFlagWasMaintained), + Config: withRandomProject(projectKey, fmt.Sprintf(testAccFeatureFlagMaintainerComputed, randomName)), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "maintainer_id", ""), + // when removed it should reset back to the most recently-set maintainer + resource.TestCheckResourceAttrPair(resourceName, "maintainer_id", "launchdarkly_team_member.test", "id"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagMaintainerDeleted), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + // it will still be set to the most recently set one even if that member has been deleted + // the UI will not show a maintainer because it will not be able to find the record post-member delete + resource.TestCheckResourceAttrSet(resourceName, "maintainer_id"), ), }, }, @@ -605,14 +660,16 @@ func TestAccFeatureFlag_InvalidMaintainer(t *testing.T) { ), }, { - Config: withRandomProject(projectKey, testAccFeatureFlagWasMaintained), + Config: withRandomProject(projectKey, testAccFeatureFlagMaintainerDeleted), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "maintainer_id", ""), + // this is the best we can do. it should default back to the most recently-set maintainer but + // we have no easy way of a + resource.TestCheckResourceAttrSet(resourceName, "maintainer_id"), ), }, }, @@ -644,20 +701,21 @@ func TestAccFeatureFlag_CreateMultivariate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.1.value", "string2"), resource.TestCheckResourceAttr(resourceName, "variations.2.value", "another option"), resource.TestCheckResourceAttr(resourceName, "tags.#", "3"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("this"), "this"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("is"), "is"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("unordered"), "unordered"), + // the v2 terraform sdk forces you to index TypeSet attributes like tags on an ordered index + resource.TestCheckResourceAttr(resourceName, "tags.0", "is"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "this"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "unordered"), resource.TestCheckResourceAttr(resourceName, "custom_properties.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "key"), "some.property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "name"), "Some Property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.#"), "3"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.0"), "value1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.1"), "value2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.2"), "value3"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "key"), "some.property2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "name"), "Some Property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "value.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "value.0"), "very special custom property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.key", "some.property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.name", "Some Property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.#", "3"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.0", "value1"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.1", "value2"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.2", "value3"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.key", "some.property2"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.name", "Some Property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.value.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.value.0", "very special custom property"), ), }, }, @@ -690,9 +748,9 @@ func TestAccFeatureFlag_CreateMultivariate2(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.1.value", "123"), resource.TestCheckResourceAttr(resourceName, "variations.2.value", "123456789"), resource.TestCheckResourceAttr(resourceName, "tags.#", "3"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("this"), "this"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("is"), "is"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("unordered"), "unordered"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "is"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "this"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "unordered"), ), }, }, @@ -720,16 +778,16 @@ func TestAccFeatureFlag_UpdateMultivariate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.1.value", "string2"), resource.TestCheckResourceAttr(resourceName, "variations.2.value", "another option"), resource.TestCheckResourceAttr(resourceName, "custom_properties.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "key"), "some.property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "name"), "Some Property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.#"), "3"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.0"), "value1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.1"), "value2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.2"), "value3"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "key"), "some.property2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "name"), "Some Property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "value.#"), "1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property2", "value.0"), "very special custom property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.key", "some.property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.name", "Some Property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.#", "3"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.0", "value1"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.1", "value2"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.2", "value3"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.key", "some.property2"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.name", "Some Property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.value.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.1.value.0", "very special custom property"), ), }, { @@ -752,15 +810,17 @@ func TestAccFeatureFlag_UpdateMultivariate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.3.name", "the new variation"), resource.TestCheckResourceAttr(resourceName, "variations.3.description", "This one was added upon update"), resource.TestCheckResourceAttr(resourceName, "tags.#", "3"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("this"), "this"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("is"), "is"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("unordered"), "unordered"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "is"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "this"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "unordered"), resource.TestCheckResourceAttr(resourceName, "custom_properties.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "key"), "some.property"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "name"), "Some Property Updated"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.#"), "2"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.0"), "value1"), - resource.TestCheckResourceAttr(resourceName, testAccCustomPropertyKey("some.property", "value.1"), "value3"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.key", "some.property"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.name", "Some Property Updated"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.#", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.0", "value1"), + resource.TestCheckResourceAttr(resourceName, "custom_properties.0.value.1", "value3"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), }, { @@ -775,28 +835,14 @@ func TestAccFeatureFlag_UpdateMultivariate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.0.value", "string1"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "string2"), resource.TestCheckResourceAttr(resourceName, "variations.2.value", "another option"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), }, }, }) } -func TestAccFeatureFlag_DefaultsInvalid(t *testing.T) { - projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: withRandomProject(projectKey, testAccFeatureFlagDefaultsMissingOffInvalid), - ExpectError: regexp.MustCompile(`invalid default variations: default_on_variation "a" is not defined as a variation`), - }, - }, - }) -} - func TestAccFeatureFlag_UpdateDefaults(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_feature_flag.defaults" @@ -811,8 +857,8 @@ func TestAccFeatureFlag_UpdateDefaults(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "true"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "false"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "0"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), }, { @@ -820,8 +866,8 @@ func TestAccFeatureFlag_UpdateDefaults(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "true"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "true"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "0"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "0"), ), }, { @@ -848,8 +894,8 @@ func TestAccFeatureFlag_UpdateMultivariateDefaults(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "b"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "b"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "1"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), }, { @@ -857,8 +903,8 @@ func TestAccFeatureFlag_UpdateMultivariateDefaults(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "c"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "c"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "2"), ), }, { @@ -866,15 +912,14 @@ func TestAccFeatureFlag_UpdateMultivariateDefaults(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "default_on_variation", "c"), - resource.TestCheckResourceAttr(resourceName, "default_off_variation", "c"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "2"), + resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "2"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"default"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -911,10 +956,6 @@ func TestAccFeatureFlag_EmptyStringVariation(t *testing.T) { }) } -func testAccCustomPropertyKey(key string, subKey string) string { - return fmt.Sprintf("custom_properties.%d.%s", hashcode.String(key), subKey) -} - func testAccCheckFeatureFlagExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index ac864859..5713cbc1 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -1,11 +1,12 @@ package launchdarkly import ( + "context" "fmt" "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -18,7 +19,7 @@ func resourceProject() *schema.Resource { Exists: resourceProjectExists, Importer: &schema.ResourceImporter{ - State: resourceProjectImport, + StateContext: resourceProjectImport, }, Schema: map[string]*schema.Schema{ @@ -38,11 +39,12 @@ func resourceProject() *schema.Resource { Type: schema.TypeBool, Optional: true, Description: "Whether feature flags created under the project should be available to client-side SDKs by default", + Default: false, }, TAGS: tagsSchema(), ENVIRONMENTS: { Type: schema.TypeList, - Optional: true, + Required: true, Description: "List of nested `environments` blocks describing LaunchDarkly environments that belong to the project", Computed: false, Elem: &schema.Resource{ @@ -110,43 +112,61 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { return fmt.Errorf("failed to update project with key %q: %s", projectKey, handleLdapiErr(err)) } // Update environments if necessary - schemaEnvList, environmentsFound := d.GetOk(ENVIRONMENTS) - if environmentsFound { - // Get the project so we can see if we need to create any environments or just update existing environments - rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey) + schemaEnvList := d.Get(ENVIRONMENTS) + // Get the project so we can see if we need to create any environments or just update existing environments + rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { + return client.ld.ProjectsApi.GetProject(client.ctx, projectKey) + }) + if err != nil { + return fmt.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) + } + project := rawProject.(ldapi.Project) + + environmentConfigs := schemaEnvList.([]interface{}) + // save envs in a key:config map so we can more easily figure out which need to be patchRemoved after + var envConfigsForCompare = make(map[string]map[string]interface{}, len(environmentConfigs)) + for _, env := range environmentConfigs { + + envConfig := env.(map[string]interface{}) + envKey := envConfig[KEY].(string) + envConfigsForCompare[envKey] = envConfig + // Check if the environment already exists. If it does not exist, create it + exists := environmentExistsInProject(project, envKey) + if !exists { + envPost := environmentPostFromResourceData(env) + _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { + return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey, envPost) + }) + if err != nil { + return fmt.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) + } + } + + // by default patching an env that was not recently tracked in the state will import it into the tf state + patches := getEnvironmentUpdatePatches(envConfig) + _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { + return handleNoConflict(func() (interface{}, *http.Response, error) { + return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey, patches) + }) }) if err != nil { - return fmt.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) + return fmt.Errorf("failed to update environment with key %q for project: %q: %+v", envKey, projectKey, err) } - project := rawProject.(ldapi.Project) - - environmentConfigs := schemaEnvList.([]interface{}) - for _, env := range environmentConfigs { - envConfig := env.(map[string]interface{}) - envKey := envConfig[KEY].(string) - // Check if the environment already exists. If it does not exist, create it - exists := environmentExistsInProject(project, envKey) - if !exists { - envPost := environmentPostFromResourceData(env) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey, envPost) - }) - if err != nil { - return fmt.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) - } - } - - patches := getEnvironmentUpdatePatches(envConfig) + } + // we also want to delete environments that were previously tracked in state and have been removed from the config + old, _ := d.GetChange(ENVIRONMENTS) + oldEnvs := old.([]interface{}) + for _, env := range oldEnvs { + envConfig := env.(map[string]interface{}) + envKey := envConfig[KEY].(string) + if _, persists := envConfigsForCompare[envKey]; !persists { _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey, patches) - }) + res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey) + return nil, res, err }) if err != nil { - return fmt.Errorf("failed to update environment with key %q for project: %q: %+v", envKey, projectKey, err) + return fmt.Errorf("failed to delete environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) } - } } @@ -188,7 +208,7 @@ func projectExists(projectKey string, meta *Client) (bool, error) { return true, nil } -func resourceProjectImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { +func resourceProjectImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { _ = d.Set(KEY, d.Id()) return []*schema.ResourceData{d}, nil diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index 823d9711..b1a96a90 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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" ) // Project resources should be formatted with a random project key because acceptance tests @@ -16,7 +16,13 @@ const ( resource "launchdarkly_project" "test" { key = "%s" name = "test project" + include_in_snippet = false tags = [ "terraform", "test" ] + environments { + name = "Test Environment" + key = "test-env" + color = "010101" + } } ` testAccProjectUpdate = ` @@ -24,7 +30,24 @@ resource "launchdarkly_project" "test" { key = "%s" name = "awesome test project" include_in_snippet = true - tags = [] + tags = [ "terraform" ] + environments { + name = "Test Environment 2.0" + key = "test-env" + color = "020202" + } +} +` + + testAccProjectUpdateRemoveOptional = ` +resource "launchdarkly_project" "test" { + key = "%s" + name = "awesome test project" + environments { + name = "Test Environment 2.0" + key = "test-env" + color = "020202" + } } ` @@ -64,6 +87,18 @@ resource "launchdarkly_project" "env_test" { tags = ["new"] } } +` + + testAccProjectWithEnvironmentUpdateRemove = ` +resource "launchdarkly_project" "env_test" { + key = "%s" + name = "test project" + environments { + key = "test-env" + name = "test environment updated" + color = "AAAAAA" + } +} ` ) @@ -83,14 +118,17 @@ func TestAccProject_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "key", projectKey), resource.TestCheckResourceAttr(resourceName, "name", "test project"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("test"), "test"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "test"), ), }, { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, + // we currently do not set the environments attr in the importer function because + // we are not forcing a complete list of nested environments on imported resource + ImportStateVerifyIgnore: []string{ENVIRONMENTS}, }, }, }) @@ -112,8 +150,13 @@ func TestAccProject_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "key", projectKey), resource.TestCheckResourceAttr(resourceName, "name", "test project"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("test"), "test"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "test"), + resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), + resource.TestCheckResourceAttr(resourceName, "environments.0.name", "Test Environment"), + resource.TestCheckResourceAttr(resourceName, "environments.0.key", "test-env"), + resource.TestCheckResourceAttr(resourceName, "environments.0.color", "010101"), ), }, { @@ -122,8 +165,24 @@ func TestAccProject_Update(t *testing.T) { testAccCheckProjectExists(resourceName), resource.TestCheckResourceAttr(resourceName, "key", projectKey), resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), - resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), + resource.TestCheckResourceAttr(resourceName, "environments.0.name", "Test Environment 2.0"), + resource.TestCheckResourceAttr(resourceName, "environments.0.key", "test-env"), + resource.TestCheckResourceAttr(resourceName, "environments.0.color", "020202"), + ), + }, + { // make sure that removal of optional attributes reverts them to their null value + Config: fmt.Sprintf(testAccProjectUpdateRemoveOptional, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), + resource.TestCheckNoResourceAttr(resourceName, "tags"), + resource.TestCheckNoResourceAttr(resourceName, "tags.#"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), ), }, }, @@ -195,6 +254,29 @@ func TestAccProject_WithEnvironments(t *testing.T) { ResourceName: resourceName, ImportState: true, }, + { + Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdateRemove, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), + + // Check that optional attributes defaulted back to false + resource.TestCheckResourceAttr(resourceName, "environments.0.name", "test environment updated"), + resource.TestCheckResourceAttr(resourceName, "environments.0.tags.#", "0"), + resource.TestCheckResourceAttr(resourceName, "environments.0.color", "AAAAAA"), + resource.TestCheckResourceAttr(resourceName, "environments.0.default_ttl", "0"), + resource.TestCheckResourceAttr(resourceName, "environments.0.secure_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.0.default_track_events", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.0.require_comments", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.0.confirm_changes", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, }, }) } diff --git a/launchdarkly/resource_launchdarkly_segment.go b/launchdarkly/resource_launchdarkly_segment.go index 920e2fcc..274ccf56 100644 --- a/launchdarkly/resource_launchdarkly_segment.go +++ b/launchdarkly/resource_launchdarkly_segment.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/resource_launchdarkly_segment_test.go b/launchdarkly/resource_launchdarkly_segment_test.go index 955a0194..2d3cde1f 100644 --- a/launchdarkly/resource_launchdarkly_segment_test.go +++ b/launchdarkly/resource_launchdarkly_segment_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( @@ -37,7 +37,6 @@ resource "launchdarkly_segment" "test" { attribute = "test_att" op = "in" values = ["test"] - negate = false } clauses { attribute = "test_att_1" @@ -104,8 +103,8 @@ func TestAccSegment_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "segment name"), resource.TestCheckResourceAttr(resourceName, "description", "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag1"), "segmentTag1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag2"), "segmentTag2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), resource.TestCheckResourceAttr(resourceName, "included.#", "2"), resource.TestCheckResourceAttr(resourceName, "included.0", "user1"), resource.TestCheckResourceAttr(resourceName, "included.1", "user2"), @@ -144,8 +143,8 @@ func TestAccSegment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "segment name"), resource.TestCheckResourceAttr(resourceName, "description", "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag1"), "segmentTag1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag2"), "segmentTag2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), resource.TestCheckResourceAttr(resourceName, "included.#", "2"), resource.TestCheckResourceAttr(resourceName, "included.0", "user1"), resource.TestCheckResourceAttr(resourceName, "included.1", "user2"), @@ -165,8 +164,8 @@ func TestAccSegment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "segment name"), resource.TestCheckResourceAttr(resourceName, "description", "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag1"), "segmentTag1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey(".segmentTag2"), ".segmentTag2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", ".segmentTag2"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag1"), resource.TestCheckResourceAttr(resourceName, "included.#", "4"), resource.TestCheckResourceAttr(resourceName, "included.0", "user1"), resource.TestCheckResourceAttr(resourceName, "included.1", "user2"), @@ -201,8 +200,8 @@ func TestAccSegment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "segment name"), resource.TestCheckResourceAttr(resourceName, "description", "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag1"), "segmentTag1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("segmentTag2"), "segmentTag2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), resource.TestCheckResourceAttr(resourceName, "included.#", "2"), resource.TestCheckResourceAttr(resourceName, "included.0", "user1"), resource.TestCheckResourceAttr(resourceName, "included.1", "user2"), diff --git a/launchdarkly/resource_launchdarkly_team_member.go b/launchdarkly/resource_launchdarkly_team_member.go index 3a9365c8..2d9bb00f 100644 --- a/launchdarkly/resource_launchdarkly_team_member.go +++ b/launchdarkly/resource_launchdarkly_team_member.go @@ -5,8 +5,8 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -20,7 +20,7 @@ func resourceTeamMember() *schema.Resource { Exists: resourceTeamMemberExists, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ diff --git a/launchdarkly/resource_launchdarkly_team_member_test.go b/launchdarkly/resource_launchdarkly_team_member_test.go index e46f2d31..87acbdf5 100644 --- a/launchdarkly/resource_launchdarkly_team_member_test.go +++ b/launchdarkly/resource_launchdarkly_team_member_test.go @@ -4,10 +4,9 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "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 ( @@ -165,7 +164,7 @@ func TestAccTeamMember_CreateWithCustomRole(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "first_name", "first"), resource.TestCheckResourceAttr(resourceName, "last_name", "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(roleKey), roleKey), + resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey), ), }, { @@ -199,7 +198,7 @@ func TestAccTeamMember_UpdateWithCustomRole(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "first_name", "first"), resource.TestCheckResourceAttr(resourceName, "last_name", "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(roleKey1), roleKey1), + resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1), ), }, { @@ -216,7 +215,7 @@ func TestAccTeamMember_UpdateWithCustomRole(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "first_name", "first"), resource.TestCheckResourceAttr(resourceName, "last_name", "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), - resource.TestCheckResourceAttr(resourceName, "custom_roles."+testAccMemberCustomRolePropertyKey(roleKey2), roleKey2), + resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey2), ), }, { @@ -228,10 +227,6 @@ func TestAccTeamMember_UpdateWithCustomRole(t *testing.T) { }) } -func testAccMemberCustomRolePropertyKey(roleKey string) string { - return fmt.Sprintf("%d", hashcode.String(roleKey)) -} - func testAccCheckMemberExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/launchdarkly/resource_launchdarkly_webhook.go b/launchdarkly/resource_launchdarkly_webhook.go index 7488ba6d..32407630 100644 --- a/launchdarkly/resource_launchdarkly_webhook.go +++ b/launchdarkly/resource_launchdarkly_webhook.go @@ -1,11 +1,10 @@ package launchdarkly import ( - "errors" "fmt" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -16,18 +15,11 @@ func resourceWebhook() *schema.Resource { Required: true, Description: "The URL of the remote webhook", } - schemaMap[ENABLED] = &schema.Schema{ - Type: schema.TypeBool, - Description: "Whether this webhook is enabled or not. This field has been deprecated in favor of 'on'", - Optional: true, - Deprecated: "'enabled' is deprecated in favor of 'on'", - ConflictsWith: []string{ON}, - } schemaMap[ON] = &schema.Schema{ - Type: schema.TypeBool, - Description: "Whether this webhook is enabled or not", - Optional: true, - ConflictsWith: []string{ENABLED}, + Type: schema.TypeBool, + Description: "Whether this webhook is enabled or not", + Optional: true, + Default: false, } return &schema.Resource{ Create: resourceWebhookCreate, @@ -37,7 +29,7 @@ func resourceWebhook() *schema.Resource { Exists: resourceWebhookExists, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Schema: schemaMap, @@ -49,15 +41,12 @@ func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { webhookURL := d.Get(URL).(string) webhookSecret := d.Get(SECRET).(string) webhookName := d.Get(NAME).(string) - statements, err := policyStatementsFromResourceData(getWebhookStatements(d)) + statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) if err != nil { return err } - webhookOn, err := getWebhookOn(d) - if err != nil { - return err - } + webhookOn := d.Get(ON).(bool) webhookBody := ldapi.WebhookBody{ Url: webhookURL, @@ -103,11 +92,7 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { webhookSecret := d.Get(SECRET).(string) webhookName := d.Get(NAME).(string) webhookTags := stringsFromResourceData(d, TAGS) - - webhookOn, err := getWebhookOn(d) - if err != nil { - return err - } + webhookOn := d.Get(ON).(bool) patch := []ldapi.PatchOperation{ patchReplace("/url", &webhookURL), @@ -117,12 +102,17 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { patchReplace("/tags", &webhookTags), } - statements, err := policyStatementsFromResourceData(getWebhookStatements(d)) + statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) if err != nil { return err } - if len(statements) > 0 { - patch = append(patch, patchReplace("/statements", &statements)) + + if d.HasChange(STATEMENTS) { + if len(statements) > 0 { + patch = append(patch, patchReplace("/statements", &statements)) + } else { + patch = append(patch, patchRemove("/statements")) + } } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { @@ -170,31 +160,3 @@ func webhookExists(webhookID string, meta *Client) (bool, error) { return true, nil } - -// getWebhookOn is a helper function used for deprecating ENABLED in favor of ON to match -// LD's API response. -func getWebhookOn(d *schema.ResourceData) (bool, error) { - var webhookOn bool - enabled, enabledSet := d.GetOkExists(ENABLED) - on, onSet := d.GetOkExists(ON) - if !onSet && !enabledSet { - return false, errors.New("one of 'on' or 'enabled' must be configured") - } - if enabledSet { - webhookOn = enabled.(bool) - } else { - webhookOn = on.(bool) - } - return webhookOn, nil -} - -// getWebhookStatements is a helper function used for deprecating POLICY_STATEMENTS in favor of STATEMENTS -// to match LD's API response. -func getWebhookStatements(d *schema.ResourceData) []interface{} { - if v, ok := d.GetOk(POLICY_STATEMENTS); ok { - return v.([]interface{}) - } else if v, ok := d.GetOk(STATEMENTS); ok { - return v.([]interface{}) - } - return make([]interface{}, 0) -} diff --git a/launchdarkly/resource_launchdarkly_webhook_test.go b/launchdarkly/resource_launchdarkly_webhook_test.go index 919fffd3..34a12d9e 100644 --- a/launchdarkly/resource_launchdarkly_webhook_test.go +++ b/launchdarkly/resource_launchdarkly_webhook_test.go @@ -5,8 +5,8 @@ import ( "regexp" "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) const ( @@ -24,7 +24,7 @@ resource "launchdarkly_webhook" "test" { name = "example-webhook" url = "http://webhooks.com" tags = [ "terraform" ] - enabled = true + on = true } ` @@ -50,13 +50,21 @@ resource "launchdarkly_webhook" "with_statements" { } } ` - // policy_statements is deprecated but we will still support it in v1 + + testAccWebhookWithStatementsRemoved = ` +resource "launchdarkly_webhook" "with_statements" { + name = "Webhook without statements" + url = "http://webhooks.com" + on = true +} +` + testAccWebhookWithPolicyStatements = ` resource "launchdarkly_webhook" "with_statements" { name = "Webhook with policy statements" url = "http://webhooks.com" on = true - policy_statements { + statements { actions = ["*"] effect = "allow" resources = ["proj/*:env/production:flag/*"] @@ -113,8 +121,8 @@ func TestAccWebhook_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, { @@ -140,10 +148,10 @@ func TestAccWebhook_CreateWithEnabled(t *testing.T) { testAccCheckWebhookExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, { @@ -168,10 +176,10 @@ func TestAccWebhook_Update(t *testing.T) { testAccCheckWebhookExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, { @@ -182,8 +190,8 @@ func TestAccWebhook_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), resource.TestCheckResourceAttr(resourceName, "on", "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, { @@ -194,10 +202,10 @@ func TestAccWebhook_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com/updatedUrl"), resource.TestCheckResourceAttr(resourceName, "on", "false"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("terraform"), "terraform"), - resource.TestCheckResourceAttr(resourceName, testAccTagKey("updated"), "updated"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), resource.TestCheckResourceAttr(resourceName, SECRET, "SuperSecret"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "0"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, { @@ -255,12 +263,12 @@ func TestAccWebhook_CreateWithPolicyStatements(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", "Webhook with policy statements"), resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), resource.TestCheckResourceAttr(resourceName, "on", "true"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.0.effect", "allow"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.#", "1"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.0", "*"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.0.resources.#", "1"), - resource.TestCheckResourceAttr(resourceName, "policy_statements.0.resources.0", "proj/*:env/production:flag/*"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/production:flag/*"), ), }, }, @@ -310,6 +318,16 @@ func TestAccWebhook_UpdateWithStatements(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "statements.1.resources.0", "proj/test:env/production:segment/*"), ), }, + { + Config: testAccWebhookWithStatementsRemoved, + Check: resource.ComposeTestCheckFunc( + testAccCheckWebhookExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Webhook without statements"), + resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), + ), + }, { ResourceName: resourceName, ImportState: true, diff --git a/launchdarkly/rollout_helper.go b/launchdarkly/rollout_helper.go index aeb89775..09a30b33 100644 --- a/launchdarkly/rollout_helper.go +++ b/launchdarkly/rollout_helper.go @@ -3,8 +3,8 @@ package launchdarkly import ( "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/rule_helper.go b/launchdarkly/rule_helper.go index 3b081c72..15256a6c 100644 --- a/launchdarkly/rule_helper.go +++ b/launchdarkly/rule_helper.go @@ -4,8 +4,8 @@ import ( "errors" "log" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) @@ -14,7 +14,6 @@ func rulesSchema() *schema.Schema { Type: schema.TypeList, Optional: true, Description: "List of logical targeting rules. You must specify either clauses or rollout weights", - Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ CLAUSES: clauseSchema(), diff --git a/launchdarkly/segment_rule_helper.go b/launchdarkly/segment_rule_helper.go index 8de293b4..232d4936 100644 --- a/launchdarkly/segment_rule_helper.go +++ b/launchdarkly/segment_rule_helper.go @@ -1,8 +1,8 @@ package launchdarkly import ( - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/segments_helper.go b/launchdarkly/segments_helper.go index 4ab611eb..97d89926 100644 --- a/launchdarkly/segments_helper.go +++ b/launchdarkly/segments_helper.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/tags_helper.go b/launchdarkly/tags_helper.go index 8361928d..8c681df5 100644 --- a/launchdarkly/tags_helper.go +++ b/launchdarkly/tags_helper.go @@ -1,7 +1,7 @@ package launchdarkly import ( - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func tagsSchema() *schema.Schema { diff --git a/launchdarkly/target_helper.go b/launchdarkly/target_helper.go index 9263105d..36f3e829 100644 --- a/launchdarkly/target_helper.go +++ b/launchdarkly/target_helper.go @@ -1,100 +1,72 @@ package launchdarkly import ( - "log" - - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go" ) func targetsSchema() *schema.Schema { return &schema.Schema{ - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, - Description: "List of nested blocks describing the individual user targets for each variation. The order of the user_targets blocks determines the index of the variation to serve if a user_target is matched", - Computed: true, + Description: "Set of nested blocks describing the individual user targets for each variation", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "values": { + VALUES: { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, + Required: true, Description: "List of user strings to target", }, + VARIATION: { + Type: schema.TypeInt, + Required: true, + Description: "Index of the variation to serve if a user_target is matched", + ValidateFunc: validation.IntAtLeast(0), + }, }, }, } } func targetsFromResourceData(d *schema.ResourceData) []ldapi.Target { - var schemaTargets []interface{} - targetsHasChange := d.HasChange(TARGETS) - userTargetsHasChange := d.HasChange(USER_TARGETS) - if targetsHasChange { - schemaTargets = d.Get(TARGETS).([]interface{}) - } else if userTargetsHasChange { - schemaTargets = d.Get(USER_TARGETS).([]interface{}) + tgts, ok := d.GetOk(TARGETS) + if !ok { + return []ldapi.Target{} } - targets := make([]ldapi.Target, len(schemaTargets)) - for i, target := range schemaTargets { - v := targetFromResourceData(i, target) - targets[i] = v + schemaTargets := tgts.(*schema.Set).List() + targets := make([]ldapi.Target, 0, len(schemaTargets)) + for _, target := range schemaTargets { + targetMap := target.(map[string]interface{}) + targets = append(targets, targetFromResourceData(targetMap)) } return targets } -func targetFromResourceData(variation int, val interface{}) ldapi.Target { - if val == nil { - return ldapi.Target{Variation: int32(variation)} - } - targetMap := val.(map[string]interface{}) - p := ldapi.Target{ - Variation: int32(variation), +func targetFromResourceData(targetMap map[string]interface{}) ldapi.Target { + resourceValues := targetMap[VALUES].([]interface{}) + values := make([]string, 0, len(resourceValues)) + for _, v := range resourceValues { + values = append(values, v.(string)) } - for _, v := range targetMap[VALUES].([]interface{}) { - p.Values = append(p.Values, v.(string)) + return ldapi.Target{ + Variation: int32(targetMap[VARIATION].(int)), + Values: values, } - - log.Printf("[DEBUG] %+v\n", p) - - return p } // targetToResourceData converts the `target` information returned // by the LaunchDarkly API into a format suitable for Terraform -// If no `targets` are specified for a given variation, LaunchDarkly may -// omit this information in the response. For example: -// "targets": [ -// { -// "values": [ -// "test" -// ], -// "variation": 1 -// } -// ], -// From this information, we must imply that variation 0 has no targets. func targetsToResourceData(targets []ldapi.Target) []interface{} { - targetMap := make(map[int32][]string, len(targets)) - maxVariationIndex := int32(-1) - - for _, p := range targets { - if p.Variation > maxVariationIndex { - maxVariationIndex = p.Variation + transformed := make([]interface{}, 0, len(targets)) + for _, target := range targets { + resourceTarget := map[string]interface{}{ + VALUES: target.Values, + VARIATION: int(target.Variation), } - targetMap[p.Variation] = p.Values + transformed = append(transformed, resourceTarget) } - transformed := make([]interface{}, maxVariationIndex+1) - - for i := int32(0); i <= maxVariationIndex; i++ { - values, found := targetMap[i] - if !found { - values = []string{} - } - transformed[i] = map[string]interface{}{ - VALUES: values, - } - } - return transformed } diff --git a/launchdarkly/target_helper_test.go b/launchdarkly/target_helper_test.go index 585e9902..d94c8268 100644 --- a/launchdarkly/target_helper_test.go +++ b/launchdarkly/target_helper_test.go @@ -27,10 +27,12 @@ func TestTargetsToResourceData(t *testing.T) { }, expected: []interface{}{ map[string]interface{}{ - "values": []string{"test1"}, + "values": []string{"test1"}, + "variation": 0, }, map[string]interface{}{ - "values": []string{"test2"}, + "values": []string{"test2"}, + "variation": 1, }, }, }, @@ -48,10 +50,12 @@ func TestTargetsToResourceData(t *testing.T) { }, expected: []interface{}{ map[string]interface{}{ - "values": []string{"test2"}, + "values": []string{"test1"}, + "variation": 1, }, map[string]interface{}{ - "values": []string{"test1"}, + "values": []string{"test2"}, + "variation": 0, }, }, }, @@ -65,10 +69,8 @@ func TestTargetsToResourceData(t *testing.T) { }, expected: []interface{}{ map[string]interface{}{ - "values": []string{}, - }, - map[string]interface{}{ - "values": []string{"test2"}, + "values": []string{"test2"}, + "variation": 1, }, }, }, diff --git a/launchdarkly/validation_helper.go b/launchdarkly/validation_helper.go index 2d6ac6a0..46ddf14e 100644 --- a/launchdarkly/validation_helper.go +++ b/launchdarkly/validation_helper.go @@ -3,8 +3,8 @@ package launchdarkly import ( "regexp" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func validateKey() schema.SchemaValidateFunc { diff --git a/launchdarkly/variations_helper.go b/launchdarkly/variations_helper.go index 1b927202..59d03c5d 100644 --- a/launchdarkly/variations_helper.go +++ b/launchdarkly/variations_helper.go @@ -7,8 +7,8 @@ import ( "strconv" "strings" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" ldapi "github.com/launchdarkly/api-client-go" ) diff --git a/launchdarkly/variations_helper_test.go b/launchdarkly/variations_helper_test.go index 4f277b6a..68f0dec4 100644 --- a/launchdarkly/variations_helper_test.go +++ b/launchdarkly/variations_helper_test.go @@ -3,7 +3,7 @@ package launchdarkly import ( "testing" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/launchdarkly/webhooks_helper.go b/launchdarkly/webhooks_helper.go index 34d9dbf5..85b74069 100644 --- a/launchdarkly/webhooks_helper.go +++ b/launchdarkly/webhooks_helper.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go" ) @@ -22,16 +22,8 @@ func baseWebhookSchema() map[string]*schema.Schema { Optional: true, Description: "A human-readable name for your webhook", }, - POLICY_STATEMENTS: policyStatementsSchema( - policyStatementSchemaOptions{ - deprecated: "'policy_statements' is deprecated in favor of 'statements'", - conflictsWith: []string{STATEMENTS}, - }, - ), - STATEMENTS: policyStatementsSchema(policyStatementSchemaOptions{ - conflictsWith: []string{POLICY_STATEMENTS}, - }), - TAGS: tagsSchema(), + STATEMENTS: policyStatementsSchema(policyStatementSchemaOptions{}), + TAGS: tagsSchema(), } } @@ -63,43 +55,12 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er } _ = d.Set(URL, webhook.Url) _ = d.Set(SECRET, webhook.Secret) - - // "enabled" is deprecated in favor of "on". For data sources, set both, for resources only set the one being used. - if isDataSource { - _ = d.Set(ENABLED, webhook.On) - _ = d.Set(ON, webhook.On) - } else { - if _, ok := d.GetOkExists(ENABLED); ok { - _ = d.Set(ENABLED, webhook.On) - } else { - _ = d.Set(ON, webhook.On) - } - } - + _ = d.Set(ON, webhook.On) _ = d.Set(NAME, webhook.Name) - // // "policy_statements" is deprecated in favor of "statements". For data sources, set both, for resources only set the one being used. - if isDataSource { - err = d.Set(POLICY_STATEMENTS, statements) - if err != nil { - return fmt.Errorf("failed to set policy_statements on webhook with id %q: %v", webhookID, err) - } - err = d.Set(STATEMENTS, statements) - if err != nil { - return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) - } - } else { - if _, ok := d.GetOk(POLICY_STATEMENTS); ok { - err = d.Set(POLICY_STATEMENTS, statements) - if err != nil { - return fmt.Errorf("failed to set policy_statements on webhook with id %q: %v", webhookID, err) - } - } else { - err = d.Set(STATEMENTS, statements) - if err != nil { - return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) - } - } + err = d.Set(STATEMENTS, statements) + if err != nil { + return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) } err = d.Set(TAGS, webhook.Tags) diff --git a/main.go b/main.go index ea77e8df..6a28484f 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/hashicorp/terraform-plugin-sdk/plugin" + "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" "github.com/launchdarkly/terraform-provider-launchdarkly/launchdarkly" ) diff --git a/website/docs/d/feature_flag.html.markdown b/website/docs/d/feature_flag.html.markdown index e6c29a46..43eec85c 100644 --- a/website/docs/d/feature_flag.html.markdown +++ b/website/docs/d/feature_flag.html.markdown @@ -38,9 +38,7 @@ In addition to the arguments above, the resource exports the following attribute - `variations` - List of nested blocks describing the variations associated with the feature flag. To learn more, read [Nested Variations Blocks](#nested-variations-blocks). -- `default_on_variation` - The value of the variation served when the flag is on for new environments. - -- `default_off_variation` - The value of the variation served when the flag is off for new environments. +- `defaults` - A map describing the index of the variation served when the flag is on for new environments. To learn more, read [Nested Defaults Blocks](#nested-defaults-blocks). - `description` - The feature flag's description. @@ -58,12 +56,20 @@ In addition to the arguments above, the resource exports the following attribute Nested `variations` blocks have the following attributes: -- `value` - The variation value. +- `value` - The variation value. - `name` - The name of the variation. - `description` - The variation's description. +### Nested Defaults Blocks + +Nested `defaults` blocks have the following structure: + +- `on_variation` - (Required) The index of the variation the flag will default to in all new environments when on. + +- `off_variation` - (Required) The index of the variation the flag will default to in all new environments when off. + ### Nested Client-Side Availibility Block The nested `client_side_availability` block has the following attributes: @@ -81,4 +87,3 @@ Nested `custom_properties` have the following attributes: - `name` - The name of the custom property. - `value` - The list of custom property value strings. - diff --git a/website/docs/d/feature_flag_environment.html.markdown b/website/docs/d/feature_flag_environment.html.markdown index a6ff47aa..82940a72 100644 --- a/website/docs/d/feature_flag_environment.html.markdown +++ b/website/docs/d/feature_flag_environment.html.markdown @@ -30,7 +30,7 @@ data "launchdarkly_feature_flag_environment" "example" { In addition to the arguments above, the resource exports the following attributes: -- `targeting_enabled` - Whether targeting is enabled. +- `on` - Whether targeting is enabled. - `track_events` - Whether event data will be sent back to LaunchDarkly. @@ -38,7 +38,7 @@ In addition to the arguments above, the resource exports the following attribute - `prerequisites` - List of nested blocks describing prerequisite feature flags rules. To learn more, read [Nested Prequisites Blocks](#nested-prerequisites-blocks). -- `targets` (previously `user_targets`) - List of nested blocks describing the individual user targets for each variation. The order of the `targets` blocks determines the index of the variation to serve if a `target` is matched. To learn more, read [Nested Target Blocks](#nested-targets-blocks). +- `targets` (previously `user_targets`) - Set of nested blocks describing the individual user targets for each variation. To learn more, read [Nested Target Blocks](#nested-targets-blocks). - `rules` - List of logical targeting rules. To learn more, read [Nested Rules Blocks](#nested-rules-blocks). @@ -58,6 +58,8 @@ Nested `targets` blocks have the following structure: - `values` - List of `user` strings to target. +- `variation` - The index of the variation to serve is a user target value is matched. + ### Nested Fallthrough Block The nested `fallthrough` block has the following structure: diff --git a/website/docs/d/project.html.markdown b/website/docs/d/project.html.markdown index b7c4dc94..ca8cf701 100644 --- a/website/docs/d/project.html.markdown +++ b/website/docs/d/project.html.markdown @@ -11,6 +11,8 @@ Provides a LaunchDarkly project data source. This data source allows you to retrieve project information from your LaunchDarkly organization. +-> **Note:** LaunchDarkly data sources do not provide access to the project's environments. If you wish to import environment configurations as data sources you must use the [`launchdarkly_environment` data source](/docs/providers/launchdarkly/d/environment.html). + ## Example Usage ```hcl diff --git a/website/docs/d/webhook.html.markdown b/website/docs/d/webhook.html.markdown index 68a3a3f5..90e9c1ec 100644 --- a/website/docs/d/webhook.html.markdown +++ b/website/docs/d/webhook.html.markdown @@ -29,7 +29,7 @@ In addition to the arguments above, the resource exports following attributes: - `url` - The URL of the remote webhook. -- `enabled` - Whether the webhook is enabled. This attribute is **deprecated** in favor or `on`. Please update all references of `enabled` to `on` to maintain compatibility with future versions. +- `on` - Whether the webhook is enabled. - `name` - The webhook's human-readable name. @@ -39,11 +39,9 @@ In addition to the arguments above, the resource exports following attributes: - `statements` - List of policy statement blocks used to filter webhook events. For more information on webhook policy filters read [Adding a policy filter](https://docs.launchdarkly.com/integrations/webhooks#adding-a-policy-filter). To learn more, read [Policy Statement Blocks](#policy-statement-blocks). -- `policy_statements` - List of policy statement blocks used to filter webhook events. For more information on webhook policy filters read [Adding a policy filter](https://docs.launchdarkly.com/integrations/webhooks#adding-a-policy-filter). To learn more, read [Policy Statement Blocks](#policy-statement-blocks). This attribute is **deprecated** in favor or `statements`. Please update all references of `policy_statements` to `statements` to maintain compatibility with future versions. +### Statement Blocks -### Policy Statement Blocks - -Webhook `statements` (previously `policy_statements`) blocks are composed of the following arguments: +Webhook `statements` blocks are composed of the following arguments: - `effect` - Either `allow` or `deny`. This argument defines whether the statement allows or denies access to the named resources and actions. diff --git a/website/docs/r/destination.html.markdown b/website/docs/r/destination.html.markdown index 89e85f37..396515a5 100644 --- a/website/docs/r/destination.html.markdown +++ b/website/docs/r/destination.html.markdown @@ -108,9 +108,7 @@ resource "launchdarkly_destination" "example" { - `config` - (Required) - The destination-specific configuration. To learn more, read [Destination-Specific Configs](#destination-specific-configs). -- `enabled` - (Optional, **Deprecated**) - Whether the data export destination is on or not. This field argument is **deprecated** in favor of `on`. Please update your config to use to `on` to maintain compatibility with future versions. - -- `on` - (Optional) - Whether the data export destination is on or not. +- `on` - (Optional, previously `enabled`) - Whether the data export destination is on or not. ### Destination-Specific Configs diff --git a/website/docs/r/environment.html.markdown b/website/docs/r/environment.html.markdown index 9e60e6a3..6d9614a9 100644 --- a/website/docs/r/environment.html.markdown +++ b/website/docs/r/environment.html.markdown @@ -9,7 +9,7 @@ description: |- Provides a LaunchDarkly environment resource. -This resource allows you to create and manage environments in your LaunchDarkly organization. +This resource allows you to create and manage environments in your LaunchDarkly organization. This resource should _not_ be used if the encapsulated project is also managed via Terraform. In this case, you should _always_ use the nested environments config blocks on your[`launchdarkly_project`](/docs/providers/launchdarkly/r/project.html) resource to manage your environments. -> **Note:** Mixing the use of nested `environments` blocks in the [`launchdarkly_project`](/docs/providers/launchdarkly/r/project.html) resource and `launchdarkly_environment` resources is not recommended. @@ -38,15 +38,15 @@ resource "launchdarkly_environment" "staging" { - `tags` - (Optional) Set of tags associated with the environment. -- `secure_mode` - (Optional) Set to `true` to ensure a user of the client-side SDK cannot impersonate another user. +- `secure_mode` - (Optional) Set to `true` to ensure a user of the client-side SDK cannot impersonate another user. This field will default to `false` when not set. -- `default_track_events` - (Optional) Set to `true` to enable data export for every flag created in this environment after you configure this argument. To learn more, read [Data Export](https://docs.launchdarkly.com/docs/data-export). +- `default_track_events` - (Optional) Set to `true` to enable data export for every flag created in this environment after you configure this argument. This field will default to `false` when not set. To learn more, read [Data Export](https://docs.launchdarkly.com/docs/data-export). -- `default_ttl` - (Optional) The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK. To learn more, read [TTL settings](https://docs.launchdarkly.com/docs/environments#section-ttl-settings). +- `default_ttl` - (Optional) The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK. This field will default to `0` when not set. To learn more, read [TTL settings](https://docs.launchdarkly.com/docs/environments#section-ttl-settings). -- `require_comments` - (Optional) Set to `true` if this environment requires comments for flag and segment changes. +- `require_comments` - (Optional) Set to `true` if this environment requires comments for flag and segment changes. This field will default to `false` when not set. -- `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. +- `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. This field will default to `false` when not set. ## Attribute Reference diff --git a/website/docs/r/feature_flag.html.markdown b/website/docs/r/feature_flag.html.markdown index 644c95c3..d88b17c9 100644 --- a/website/docs/r/feature_flag.html.markdown +++ b/website/docs/r/feature_flag.html.markdown @@ -37,8 +37,10 @@ resource "launchdarkly_feature_flag" "building_materials" { description = "The strongest variation" } - default_on_variation = "bricks" - default_off_variation = "straw" + defaults { + on_variation = 2 + off_variation = 0 + } tags = [ "example", @@ -65,8 +67,10 @@ resource "launchdarkly_feature_flag" "json_example" { value = jsonencode({ "foos" : ["bar1", "bar2"] }) } - default_on_variation = jsonencode({ "foos" : ["bar1", "bar2"] }) - default_off_variation = jsonencode({ "foo" : "bar" }) + defaults { + on_variation = 1 + off_variation = 0 + } } ``` @@ -82,15 +86,13 @@ resource "launchdarkly_feature_flag" "json_example" { - `variations` - (Required) List of nested blocks describing the variations associated with the feature flag. You must specify at least two variations. To learn more, read [Nested Variations Blocks](#nested-variations-blocks). -- `default_on_variation` - (Optional) The value of the variation served when the flag is on for new environments. Required if `default_off_variation` is set. Flag configurations in existing environments will not be changed. The value used here must be an exact match (including whitespace) to the `value` set in the `variations` list. - -- `default_off_variation` - (Optional) The value of the variation served when the flag is off for new environments. Required if `default_on_variation` is set. Flag configurations in existing environments will not be changed. The value used here must be an exact match (including whitespace) to the `value` set in the `variations` list. +- `defaults` - (Optional) A block containing the indices of the variations to be used as the default on and off variations in all new environments. Flag configurations in existing environments will not be changed nor updated if the configuration block is removed. To learn more, read [Nested Defaults Blocks](#nested-defaults-blocks). - `description` - (Optional) The feature flag's description. - `tags` - (Optional) Set of feature flag tags. -- `maintainer_id` - (Optional) The feature flag maintainer's 24 character alphanumeric team member ID. +- `maintainer_id` - (Optional) The feature flag maintainer's 24 character alphanumeric team member ID. If not set, it will automatically be or stay set to the member ID associated with the API key used by your LaunchDarkly Terraform provider or the most recently-set maintainer. - `temporary` - (Optional) Specifies whether the flag is a temporary flag. @@ -116,6 +118,14 @@ variations { - `description` - (Optional) The variation's description. +### Nested Defaults Blocks + +Nested `defaults` blocks have the following structure: + +- `on_variation` - (Required) The index of the variation the flag will default to in all new environments when on. + +- `off_variation` - (Required) The index of the variation the flag will default to in all new environments when off. + ### Nested Custom Properties Nested `custom_properties` have the following structure: diff --git a/website/docs/r/feature_flag_environment.html.markdown b/website/docs/r/feature_flag_environment.html.markdown index b0bbd38b..59a0e4cd 100644 --- a/website/docs/r/feature_flag_environment.html.markdown +++ b/website/docs/r/feature_flag_environment.html.markdown @@ -25,14 +25,13 @@ resource "launchdarkly_feature_flag_environment" "number_env" { variation = 0 } - user_targets { - values = ["user0"] - } - user_targets { - values = ["user1", "user2"] + targets { + values = ["user0"] + variation = 0 } - user_targets { - values = [] + targets { + values = ["user1", "user2"] + variation = 1 } rules { @@ -54,6 +53,7 @@ resource "launchdarkly_feature_flag_environment" "number_env" { fallthrough { rollout_weights = [60000, 40000, 0] } + off_variation = 2 } ``` @@ -63,23 +63,19 @@ resource "launchdarkly_feature_flag_environment" "number_env" { - `env_key` - (Required) The environment key. -- `targeting_enabled` - (Optional, **Deprecated**) Whether targeting is enabled. This field argument is **deprecated** in favor of `on`. Please update your config to use to `on` to maintain compatibility with future versions. Either `on` or `targeting_enabled` must be specified. - -- `on` - (Optional) Whether targeting is enabled. +- `on` (previously `targeting_enabled`) - (Optional) Whether targeting is enabled. Defaults to `false` if not set. -- `track_events` - (Optional) Whether to send event data back to LaunchDarkly. +- `track_events` - (Optional) Whether to send event data back to LaunchDarkly. Defaults to `false` if not set. -- `off_variation` - (Optional) The index of the variation to serve if targeting is disabled. +- `off_variation` - (Required) The index of the variation to serve if targeting is disabled. - `prerequisites` - (Optional) List of nested blocks describing prerequisite feature flags rules. To learn more, read [Nested Prequisites Blocks](#nested-prerequisites-blocks). -- `user_targets` - (Optional) List of nested blocks describing the individual user targets for each variation. The order of the `user_targets` blocks determines the index of the variation to serve if a `user_target` is matched. To learn more, read [Nested User Target Blocks](#nested-user-targets-blocks). +- `targets` (previously `user_targets`) - (Optional) Set of nested blocks describing the individual user targets for each variation. To learn more, read [Nested Target Blocks](#nested-targets-blocks). - `rules` - (Optional) List of logical targeting rules. To learn more, read [Nested Rules Blocks](#nested-rules-blocks). -- `flag_fallthrough` - (Optional, **Deprecated**) Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply. This attribute is **deprecated** in favor of `fallthrough`. Please update all references of `flag_fallthrough` to `fallthrough` to maintain compatibility with future versions. - -- `fallthrough` - (Optional) Nested block describing the default variation to serve if no `prerequisites`, `user_target`, or `rules` apply.To learn more, read [Nested Fallthrough Block](#nested-fallthrough-block). +- `fallthrough` (previously `flag_fallthrough`) - (Required) Nested block describing the default variation to serve if no `prerequisites`, `target`, or `rules` apply.To learn more, read [Nested Fallthrough Block](#nested-fallthrough-block). ### Nested Prerequisites Blocks @@ -89,19 +85,21 @@ Nested `prerequisites` blocks have the following structure: - `variation` - (Required) The index of the prerequisite feature flag's variation to target. -### Nested User Targets Blocks +### Nested Targets Blocks + +Nested `targets` blocks have the following structure: -Nested `user_targets` blocks have the following structure: +- `values` - (Required) List of `user` strings to target. -- `values` - (Optional) List of `user` strings to target. +- `variation` - (Required) The index of the variation to serve is a user target value is matched. ### Nested Fallthrough Block The nested `fallthrough` (previously `flag_fallthrough`) block has the following structure: -- `variation` - (Optional) The default integer variation index to serve if no `prerequisites`, `user_target`, or `rules` apply. You must specify either `variation` or `rollout_weights`. +- `variation` - (Optional) The default integer variation index to serve if no `prerequisites`, `target`, or `rules` apply. You must specify either `variation` or `rollout_weights`. -- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if no `prerequisites`, `user_target`, or `rules` apply. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. +- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if no `prerequisites`, `target`, or `rules` apply. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. - `bucket_by` - (Optional) Group percentage rollout by a custom attribute. This argument is only valid if `rollout_weights` is also specified. @@ -131,6 +129,12 @@ Nested `clauses` blocks have the following structure: - `negate` - (Required) Whether to negate the rule clause. +Nested `fallthrough` blocks have the following structure: + +- `variation` - (Optional) The integer variation index to serve if the rule clauses evaluate to `true`. You must specify either `variation` or `rollout_weights`. + +- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. + ## Attributes Reference In addition to the arguments above, the resource exports the following attribute: diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 2c6ce5dd..0d785c66 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -44,13 +44,13 @@ resource "launchdarkly_project" "example" { - `name` - (Required) The project's name. -- `include_in_snippet - (Optional) Whether feature flags created under the project should be available to client-side SDKs by default. +- `environments` - (Required) List of nested `environments` blocks describing LaunchDarkly environments that belong to the project. When managing LaunchDarkly projects in Terraform, you should always manage your environments as nested project resources. To learn more, read [Nested Environments Blocks](#nested-environments-blocks). -- `tags` - (Optional) The project's set of tags. +-> **Note:** Mixing the use of nested `environments` blocks and [`launchdarkly_environment`](/docs/providers/launchdarkly/r/environment.html) resources is not recommended. `launchdarkly_environment` resources should only be used when the encapsulating project is not managed in Terraform. -- `environments` - (Optional) List of nested `environments` blocks describing LaunchDarkly environments that belong to the project. Use the nested `environments` blocks instead of the `launchdarkly_environment` resource when you wish to override the default behavior of creating `Test` and `Production` environments during project creation. To learn more, read [Nested Environments Blocks](#nested-environments-blocks). +- `include_in_snippet - (Optional) Whether feature flags created under the project should be available to client-side SDKs by default. --> **Note:** Mixing the use of nested `environments` blocks and [`launchdarkly_environment`](/docs/providers/launchdarkly/r/environment.html) resources is not recommended. +- `tags` - (Optional) The project's set of tags. ### Nested Environments Blocks @@ -64,15 +64,15 @@ Nested `environments` blocks have the following structure: - `tags` - (Optional) Set of tags associated with the environment. -- `secure_mode` - (Optional) Set to `true` to ensure a user of the client-side SDK cannot impersonate another user. +- `secure_mode` - (Optional) Set to `true` to ensure a user of the client-side SDK cannot impersonate another user. This field will default to `false` when not set. -- `default_track_events` - (Optional) Set to `true` to enable data export for every flag created in this environment after you configure this argument. To learn more, read [Data Export](https://docs.launchdarkly.com/docs/data-export). +- `default_track_events` - (Optional) Set to `true` to enable data export for every flag created in this environment after you configure this argument. This field will default to `false` when not set. To learn more, read [Data Export](https://docs.launchdarkly.com/docs/data-export). -- `default_ttl` - (Optional) The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK. To learn more, read [TTL settings](https://docs.launchdarkly.com/docs/environments#section-ttl-settings). +- `default_ttl` - (Optional) The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK. This field will default to `0` when not set. To learn more, read [TTL settings](https://docs.launchdarkly.com/docs/environments#section-ttl-settings). -- `require_comments` - (Optional) Set to `true` if this environment requires comments for flag and segment changes. +- `require_comments` - (Optional) Set to `true` if this environment requires comments for flag and segment changes. This field will default to `false` when not set. -- `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. +- `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. This field will default to `false` when not set. ## Import @@ -81,3 +81,5 @@ LaunchDarkly projects can be imported using the project's key, e.g. ``` $ terraform import launchdarkly_project.example example-project ``` + +Please note that, in order to manage an imported project resource using Terraform, you will be required to include at least one environment in your configuration. Managing environment resources with Terraform should always be done on the project unless the project is not also managed with Terraform. diff --git a/website/docs/r/team_member.html.markdown b/website/docs/r/team_member.html.markdown index 6b219db1..85c1d571 100644 --- a/website/docs/r/team_member.html.markdown +++ b/website/docs/r/team_member.html.markdown @@ -28,9 +28,9 @@ resource "launchdarkly_team_member" "example" { - `email` - (Required) The unique email address associated with the team member. -- `first_name` - (Optional) The team member's given name. +- `first_name` - (Optional) The team member's given name. Please note that, once created, this cannot be updated except by the team member themself. -- `last_name` - (Optional) The team member's family name. +- `last_name` - (Optional) The team member's family name. Please note that, once created, this cannot be updated except by the team member themself. - `role` - (Optional) The role associated with team member. Supported roles are `reader`, `writer`, or `admin`. If you don't specify a role, `reader` is assigned by default. diff --git a/website/docs/r/webhook.html.markdown b/website/docs/r/webhook.html.markdown index 304c6703..47ef5d7c 100644 --- a/website/docs/r/webhook.html.markdown +++ b/website/docs/r/webhook.html.markdown @@ -20,12 +20,12 @@ resource "launchdarkly_webhook" "example" { tags = ["terraform"] on = true - policy_statements { + statements { actions = ["*"] effect = "allow" resources = ["proj/*:env/production:flag/*"] } - policy_statements { + statements { actions = ["*"] effect = "allow" resources = resources = ["proj/test:env/production:segment/*"] @@ -37,9 +37,7 @@ resource "launchdarkly_webhook" "example" { - `url` - (Required) The URL of the remote webhook. -- `on` - (Optional) Specifies whether the webhook is enabled. Either `on` or `enabled` must be specified. - -- `enabled` - (Optional, **Deprecated**) Specifies whether the webhook is enabled. This field argument is **deprecated** in favor of `on`. Please update your config to use to `on` to maintain compatibility with future versions. Either `on` or `enabled` must be specified. +- `on` - (Required, previously `enabled`) Specifies whether the webhook is enabled. - `name` - (Optional) The webhook's human-readable name. @@ -47,11 +45,9 @@ resource "launchdarkly_webhook" "example" { - `tags` - (Optional) Set of tags associated with the webhook. -`statements` - (Optional) List of policy statement blocks used to filter webhook events. For more information on webhook policy filters read [Adding a policy filter](https://docs.launchdarkly.com/integrations/webhooks#adding-a-policy-filter). - -- `policy_statements` - (Optional, **Deprecated**) List of policy statement blocks used to filter webhook events. For more information on webhook policy filters read [Adding a policy filter](https://docs.launchdarkly.com/integrations/webhooks#adding-a-policy-filter). This argument is **deprecated** in favor of `statements`. Please update your config to use `statements` to maintain compatibility with future versions. +`statements` - (Optional, previously `policy_statements`) List of policy statement blocks used to filter webhook events. For more information on webhook policy filters read [Adding a policy filter](https://docs.launchdarkly.com/integrations/webhooks#adding-a-policy-filter). -Webhook `statements` and `policy_statements` blocks are composed of the following arguments: +Webhook `statements` blocks are composed of the following arguments: - `effect` - (Required) Either `allow` or `deny`. This argument defines whether the statement allows or denies access to the named resources and actions. From 2762d147e18ee9e8b847f3278270f34cf67acc05 Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 15 Sep 2021 15:12:04 +0100 Subject: [PATCH 06/36] Fix import of launchdarkly_project nested environments (#153) * start importing all environments * Preserve environment order * Update changelog * Add ignore_changes to withRandomProject --- CHANGELOG.md | 6 ++++++ launchdarkly/project_helper.go | 14 ++++++++++++++ .../resource_launchdarkly_feature_flag_test.go | 3 +++ .../resource_launchdarkly_project_test.go | 18 +++++++++++------- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2073ff..e314a27e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [Unreleased] () + +BUG FIXES: + +- Fixed [a bug](https://github.com/launchdarkly/terraform-provider-launchdarkly/issues/67) resulting in nested environments not being imported on the `launchdarkly_project` resource. As a result, _all_ of a project's environments will be saved to the Terraform state during an import of the `launchdarkly_project` resource. Please keep in mind if you have not added all of the existing environments to your Terraform config before importing a `launchdarkly_project` resource, Terraform will delete these environments from LaunchDarkly during the next `terraform apply`. If you wish to manage project properties with Terraform but not nested environments consider using Terraform's [ignore changes](https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes) lifecycle meta-argument. + ## [2.0.0] (August 31, 2021) ENHANCEMENTS: diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index 439d6678..566305e2 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -44,10 +44,24 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er // iterate over the environment keys in the order defined by the config and look up the environment returned by // LD's API rawEnvs := d.Get(ENVIRONMENTS).([]interface{}) + envConfigKeys := rawEnvironmentConfigsToKeyList(rawEnvs) + envAddedMap := make(map[string]bool, len(project.Environments)) environments := make([]interface{}, 0, len(envConfigKeys)) for _, envKey := range envConfigKeys { environments = append(environments, envMap[envKey]) + envAddedMap[envKey] = true + } + + // Now add all environments that are not specified in the config. + // This is required in order to successfully import nested environments because rawEnvs is always an empty slice + // durning import, even if nested environments are defined in the config. + for _, env := range project.Environments { + alreadyAdded := envAddedMap[env.Key] + if !alreadyAdded { + environments = append(environments, envMap[env.Key]) + envAddedMap[env.Key] = true + } } err = d.Set(ENVIRONMENTS, environments) diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index 6faaf260..89dd286d 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -408,6 +408,9 @@ resource "launchdarkly_feature_flag" "empty_string_variation" { func withRandomProject(randomProject, resource string) string { return fmt.Sprintf(` resource "launchdarkly_project" "test" { + lifecycle { + ignore_changes = [environments] + } name = "testProject" key = "%s" environments { diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index b1a96a90..b377d557 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -126,9 +126,6 @@ func TestAccProject_Create(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, - // we currently do not set the environments attr in the importer function because - // we are not forcing a complete list of nested environments on imported resource - ImportStateVerifyIgnore: []string{ENVIRONMENTS}, }, }, }) @@ -185,6 +182,11 @@ func TestAccProject_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), ), }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -218,8 +220,9 @@ func TestAccProject_WithEnvironments(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, { Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdate, projectKey), @@ -274,8 +277,9 @@ func TestAccProject_WithEnvironments(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) From 966aa6de84e10c981b7726e57242aa5478e36aae Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Mon, 20 Sep 2021 15:59:23 +0100 Subject: [PATCH 07/36] update doc (#154) --- website/docs/r/project.html.markdown | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 0d785c66..8d073dac 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -82,4 +82,17 @@ LaunchDarkly projects can be imported using the project's key, e.g. $ terraform import launchdarkly_project.example example-project ``` -Please note that, in order to manage an imported project resource using Terraform, you will be required to include at least one environment in your configuration. Managing environment resources with Terraform should always be done on the project unless the project is not also managed with Terraform. +**IMPORTANT:** Please note that, regardless of how many `environments` blocks you include on your import, _all_ of the project's environments will be saved to the Terraform state and will update with subsequent applies. This means that any environments not included in your import configuration will be torn down with any subsequent apply. If you wish to manage project properties with Terraform but not nested environments consider using Terraform's [ignore changes](https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes) lifecycle meta-argument; see below for example. + +``` +resource "launchdarkly_project" "example" { + lifecycle { + ignore_changes = [environments] + } + name = "testProject" + key = "%s" + # environments not included on this configuration will not be affected by subsequent applies + } +``` + +Managing environment resources with Terraform should always be done on the project unless the project is not also managed with Terraform. From fe9668d9cb8c7fe673e11171a66309b980190030 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Mon, 27 Sep 2021 17:05:33 +0100 Subject: [PATCH 08/36] Imiller/sc 119920/add boolean archive attribute to launchdarkly (#155) * add archived to ff env schema * make it a global property * put it in the wrong schema doh * attempt archive before deleting so we can return an error if there are dependencies * prelim test * some syntax issues * gotta figure out error bit * remove failing test * forgot to set archived in read function * actually no need to archive first --- launchdarkly/feature_flags_helper.go | 7 +++++++ launchdarkly/keys.go | 1 + launchdarkly/resource_launchdarkly_feature_flag.go | 2 ++ launchdarkly/resource_launchdarkly_feature_flag_test.go | 7 +++---- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/launchdarkly/feature_flags_helper.go b/launchdarkly/feature_flags_helper.go index 354f0045..45f54449 100644 --- a/launchdarkly/feature_flags_helper.go +++ b/launchdarkly/feature_flags_helper.go @@ -77,6 +77,12 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { }, }, }, + ARCHIVED: { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to archive the flag", + Default: false, + }, } } @@ -105,6 +111,7 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) _ = d.Set(DESCRIPTION, flag.Description) _ = d.Set(INCLUDE_IN_SNIPPET, flag.IncludeInSnippet) _ = d.Set(TEMPORARY, flag.Temporary) + _ = d.Set(ARCHIVED, flag.Archived) if isDataSource { CSA := *flag.ClientSideAvailability diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 0429b811..3faaad95 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -77,4 +77,5 @@ const ( EXPIRE = "expire" ID = "id" CLIENT_SIDE_AVAILABILITY = "client_side_availability" + ARCHIVED = "archived" ) diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 5449abc0..44480383 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -108,6 +108,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro includeInSnippet := d.Get(INCLUDE_IN_SNIPPET).(bool) temporary := d.Get(TEMPORARY).(bool) customProperties := customPropertiesFromResourceData(d) + archived := d.Get(ARCHIVED).(bool) patch := ldapi.PatchComment{ Comment: "Terraform", @@ -118,6 +119,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro patchReplace("/includeInSnippet", includeInSnippet), patchReplace("/temporary", temporary), patchReplace("/customProperties", customProperties), + patchReplace("/archived", archived), }} variationPatches, err := variationPatchesFromResourceData(d) diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index 89dd286d..44768c8a 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -874,10 +874,9 @@ func TestAccFeatureFlag_UpdateDefaults(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"default"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) From a298a44f7a1c9deb3736ee9c70d33f5ea33c765b Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Thu, 30 Sep 2021 16:24:23 +0100 Subject: [PATCH 09/36] Update index of docs with recommended version (#156) * Update index of docs with recommended version * Show how to configure the access token --- website/docs/index.html.markdown | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index d1054e63..6aaaed15 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -12,9 +12,17 @@ description: |- ## Example Usage ```hcl +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } +} + # Configure the LaunchDarkly provider provider "launchdarkly" { - version = "~> 1.0" access_token = var.launchdarkly_access_token } @@ -29,21 +37,6 @@ resource "launchdarkly_feature_flag" "terraform" { } ``` -If you are using Terraform 0.13 and above, the provider declaration should go directly in your `terraform` block. Additionally, the syntax will be slightly different: - -```hcl -# Configure the LaunchDarkly provider -terraform { - required_providers { - launchdarkly = { - source = "launchdarkly/launchdarkly" - version = "~> 1.0" - } - } - required_version = "~> 0.13.0" -} -``` - Please refer to [Terraform's documentation on upgrading to v0.13](https://www.terraform.io/upgrade-guides/0-13.html) for more information. ## Argument Reference From 358b5328c2248c60783cb46ca474df55385db50f Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Fri, 8 Oct 2021 10:01:54 -0400 Subject: [PATCH 10/36] Imiller/sc 123568/add approval settings to environment resource (#157) * prelim schema * tweak approval schema * draft conversion functions * i forgot you can't use conflictswith with nested attributes * a mess of things * fixed up patches * handle patch remove * okay i think this should handle all cases * unused transformation function since no setting * forgot to handle most obvious case * need to set but only sometimes * min num approvals cannot exceed 5 * make min num approvals required * project test * fix helper function for project - first pass * make entire approvals schema computed * projet tests working * NOW project tests working * remove dead code * update docs * update changelog * simplify approval helper function * switch old and new afunc arguments --- CHANGELOG.md | 8 + launchdarkly/approvals_helper.go | 123 ++++++++++++++ launchdarkly/environments_helper.go | 21 ++- launchdarkly/keys.go | 156 +++++++++--------- .../resource_launchdarkly_environment.go | 22 ++- .../resource_launchdarkly_environment_test.go | 103 ++++++++++++ launchdarkly/resource_launchdarkly_project.go | 20 ++- .../resource_launchdarkly_project_test.go | 74 ++++++++- website/docs/r/environment.html.markdown | 34 ++++ website/docs/r/project.html.markdown | 22 +++ 10 files changed, 498 insertions(+), 85 deletions(-) create mode 100644 launchdarkly/approvals_helper.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0e01d8..d3c89027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [Unreleased] + +FEATURES: + +- Added `approval_settings` blocks to the `launchdarkly_environment` resource and nested `environments` blocks on the `launchdarkly_project` resource. + +- Added a boolean `archive` attribute on the `launchdarkly_feature_flag` resource to allow archiving and unarchiving flags instead of deleting them. + ## [2.0.1] (September 20, 2021) BUG FIXES: diff --git a/launchdarkly/approvals_helper.go b/launchdarkly/approvals_helper.go new file mode 100644 index 00000000..e4120970 --- /dev/null +++ b/launchdarkly/approvals_helper.go @@ -0,0 +1,123 @@ +package launchdarkly + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + ldapi "github.com/launchdarkly/api-client-go" +) + +func approvalSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + REQUIRED: { + Type: schema.TypeBool, + Optional: true, + Description: "Whether any changes to flags in this environment will require approval. You may only set required or requiredApprovalTags, not both.", + Default: false, + }, + CAN_REVIEW_OWN_REQUEST: { + Type: schema.TypeBool, + Optional: true, + Description: "Whether requesters can approve or decline their own request. They may always comment.", + Default: false, + }, + MIN_NUM_APPROVALS: { + Type: schema.TypeInt, + Optional: true, + Description: "The number of approvals required before an approval request can be applied.", + ValidateFunc: validation.IntBetween(1, 5), + Default: 1, + }, + CAN_APPLY_DECLINED_CHANGES: { + Type: schema.TypeBool, + Optional: true, + Description: "Whether changes can be applied as long as minNumApprovals is met, regardless of whether any reviewers have declined a request.", + Default: false, + }, + REQUIRED_APPROVAL_TAGS: { + Type: schema.TypeList, + Optional: true, + Description: "An array of tags used to specify which flags with those tags require approval. You may only set requiredApprovalTags or required, not both.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateTags(), + }, + }, + }, + }, + } +} + +func approvalSettingsFromResourceData(val interface{}) (ldapi.EnvironmentApprovalSettings, error) { + raw := val.([]interface{}) + if len(raw) == 0 { + return ldapi.EnvironmentApprovalSettings{}, nil + } + approvalSettingsMap := raw[0].(map[string]interface{}) + settings := ldapi.EnvironmentApprovalSettings{ + CanReviewOwnRequest: approvalSettingsMap[CAN_REVIEW_OWN_REQUEST].(bool), + MinNumApprovals: int64(approvalSettingsMap[MIN_NUM_APPROVALS].(int)), + CanApplyDeclinedChanges: approvalSettingsMap[CAN_APPLY_DECLINED_CHANGES].(bool), + } + // Required and RequiredApprovalTags should never be defined simultaneously + // unfortunately since they default to their null values and are nested we cannot tell if the + // user has put a value in their config, so we'll check this way + required := approvalSettingsMap[REQUIRED].(bool) + tags := approvalSettingsMap[REQUIRED_APPROVAL_TAGS].([]interface{}) + if len(tags) > 0 { + if required { + return ldapi.EnvironmentApprovalSettings{}, fmt.Errorf("invalid approval_settings config: required and required_approval_tags cannot be set simultaneously") + } + stringTags := make([]string, len(tags)) + for i := range tags { + stringTags[i] = tags[i].(string) + } + settings.RequiredApprovalTags = stringTags + } else { + settings.Required = required + } + return settings, nil +} + +func approvalSettingsToResourceData(settings ldapi.EnvironmentApprovalSettings) interface{} { + transformed := map[string]interface{}{ + CAN_REVIEW_OWN_REQUEST: settings.CanReviewOwnRequest, + MIN_NUM_APPROVALS: settings.MinNumApprovals, + CAN_APPLY_DECLINED_CHANGES: settings.CanApplyDeclinedChanges, + REQUIRED_APPROVAL_TAGS: settings.RequiredApprovalTags, + REQUIRED: settings.Required, + } + return []map[string]interface{}{transformed} +} + +func approvalPatchFromSettings(oldApprovalSettings, newApprovalSettings interface{}) ([]ldapi.PatchOperation, error) { + settings, err := approvalSettingsFromResourceData(newApprovalSettings) + if err != nil { + return []ldapi.PatchOperation{}, err + } + new := newApprovalSettings.([]interface{}) + old := oldApprovalSettings.([]interface{}) + if len(new) == 0 && len(old) == 0 { + return []ldapi.PatchOperation{}, nil + } + if len(new) == 0 && len(old) > 0 { + return []ldapi.PatchOperation{ + patchRemove("/approvalSettings/required"), + patchRemove("/approvalSettings/requiredApprovalTags"), + }, nil + } + patch := []ldapi.PatchOperation{ + patchReplace("/approvalSettings/required", settings.Required), + patchReplace("/approvalSettings/canReviewOwnRequest", settings.CanReviewOwnRequest), + patchReplace("/approvalSettings/minNumApprovals", settings.MinNumApprovals), + patchReplace("/approvalSettings/canApplyDeclinedChanges", settings.CanApplyDeclinedChanges), + patchReplace("/approvalSettings/requiredApprovalTags", settings.RequiredApprovalTags), + } + return patch, nil +} diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index ffb10871..4e022c99 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -70,11 +70,12 @@ func baseEnvironmentSchema(forProject bool) map[string]*schema.Schema { Optional: true, Description: "Whether or not to require confirmation for flag and segment changes in this environment", }, - TAGS: tagsSchema(), + TAGS: tagsSchema(), + APPROVAL_SETTINGS: approvalSchema(), } } -func getEnvironmentUpdatePatches(config map[string]interface{}) []ldapi.PatchOperation { +func getEnvironmentUpdatePatches(oldConfig, config map[string]interface{}) ([]ldapi.PatchOperation, error) { // Always include required fields name := config[NAME] color := config[COLOR] @@ -114,7 +115,18 @@ func getEnvironmentUpdatePatches(config map[string]interface{}) []ldapi.PatchOpe envTags := stringsFromSchemaSet(tags.(*schema.Set)) patches = append(patches, patchReplace("/tags", &envTags)) } - return patches + + var oldApprovalSettings []interface{} + if oldSettings, ok := oldConfig[APPROVAL_SETTINGS]; ok { + oldApprovalSettings = oldSettings.([]interface{}) + } + newApprovalSettings := config[APPROVAL_SETTINGS] + approvalPatches, err := approvalPatchFromSettings(oldApprovalSettings, newApprovalSettings) + if err != nil { + return []ldapi.PatchOperation{}, err + } + patches = append(patches, approvalPatches...) + return patches, nil } func environmentSchema(forProject bool) map[string]*schema.Schema { @@ -193,6 +205,7 @@ func environmentToResourceData(env ldapi.Environment) envResourceData { REQUIRE_COMMENTS: env.RequireComments, CONFIRM_CHANGES: env.ConfirmChanges, TAGS: env.Tags, + APPROVAL_SETTINGS: approvalSettingsToResourceData(*env.ApprovalSettings), } } @@ -237,5 +250,7 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool _ = d.Set(TAGS, env.Tags) _ = d.Set(REQUIRE_COMMENTS, env.RequireComments) _ = d.Set(CONFIRM_CHANGES, env.ConfirmChanges) + _ = d.Set(APPROVAL_SETTINGS, approvalSettingsToResourceData(*env.ApprovalSettings)) + return nil } diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 3faaad95..12890b49 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -3,79 +3,85 @@ package launchdarkly const ( // keys used in terraform files referencing keys in launchdarkly resource objects. // The name of each constant is the same as its value. - PROJECT_KEY = "project_key" - ENV_KEY = "env_key" - KEY = "key" - FLAG_ID = "flag_id" - NAME = "name" - TAGS = "tags" - ENVIRONMENTS = "environments" - API_KEY = "api_key" - MOBILE_KEY = "mobile_key" - CLIENT_SIDE_ID = "client_side_id" - COLOR = "color" - DEFAULT_TTL = "default_ttl" - SECURE_MODE = "secure_mode" - DEFAULT_TRACK_EVENTS = "default_track_events" - REQUIRE_COMMENTS = "require_comments" - CONFIRM_CHANGES = "confirm_changes" - DESCRIPTION = "description" - MAINTAINER_ID = "maintainer_id" - VARIATION_TYPE = "variation_type" - VARIATIONS = "variations" - TEMPORARY = "temporary" - INCLUDE_IN_SNIPPET = "include_in_snippet" - VALUE = "value" - URL = "url" - SECRET = "secret" - ENABLED = "enabled" - ON = "on" - RESOURCES = "resources" - NOT_RESOURCES = "not_resources" - ACTIONS = "actions" - NOT_ACTIONS = "not_actions" - EFFECT = "effect" - POLICY = "policy" - STATEMENTS = "statements" - POLICY_STATEMENTS = "policy_statements" - INLINE_ROLES = "inline_roles" - EXCLUDED = "excluded" - INCLUDED = "included" - CREATION_DATE = "creation_date" - CUSTOM_PROPERTIES = "custom_properties" - EMAIL = "email" - FIRST_NAME = "first_name" - LAST_NAME = "last_name" - ROLE = "role" - CUSTOM_ROLES = "custom_roles" - RULES = "rules" - ATTRIBUTE = "attribute" - OP = "op" - VALUES = "values" - VALUE_TYPE = "value_type" - NEGATE = "negate" - CLAUSES = "clauses" - WEIGHT = "weight" - BUCKET_BY = "bucket_by" - ROLLOUT_WEIGHTS = "rollout_weights" - VARIATION = "variation" - TARGETS = "targets" - PREREQUISITES = "prerequisites" - FLAG_KEY = "flag_key" - TRACK_EVENTS = "track_events" - FALLTHROUGH = "fallthrough" - KIND = "kind" - CONFIG = "config" - DEFAULT_ON_VARIATION = "default_on_variation" - DEFAULT_OFF_VARIATION = "default_off_variation" - DEFAULTS = "defaults" - ON_VARIATION = "on_variation" - OFF_VARIATION = "off_variation" - SERVICE_TOKEN = "service_token" - DEFAULT_API_VERSION = "default_api_version" - TOKEN = "token" - EXPIRE = "expire" - ID = "id" - CLIENT_SIDE_AVAILABILITY = "client_side_availability" - ARCHIVED = "archived" + PROJECT_KEY = "project_key" + ENV_KEY = "env_key" + KEY = "key" + FLAG_ID = "flag_id" + NAME = "name" + TAGS = "tags" + ENVIRONMENTS = "environments" + API_KEY = "api_key" + MOBILE_KEY = "mobile_key" + CLIENT_SIDE_ID = "client_side_id" + COLOR = "color" + DEFAULT_TTL = "default_ttl" + SECURE_MODE = "secure_mode" + DEFAULT_TRACK_EVENTS = "default_track_events" + REQUIRE_COMMENTS = "require_comments" + CONFIRM_CHANGES = "confirm_changes" + DESCRIPTION = "description" + MAINTAINER_ID = "maintainer_id" + VARIATION_TYPE = "variation_type" + VARIATIONS = "variations" + TEMPORARY = "temporary" + INCLUDE_IN_SNIPPET = "include_in_snippet" + VALUE = "value" + URL = "url" + SECRET = "secret" + ENABLED = "enabled" + ON = "on" + RESOURCES = "resources" + NOT_RESOURCES = "not_resources" + ACTIONS = "actions" + NOT_ACTIONS = "not_actions" + EFFECT = "effect" + POLICY = "policy" + STATEMENTS = "statements" + POLICY_STATEMENTS = "policy_statements" + INLINE_ROLES = "inline_roles" + EXCLUDED = "excluded" + INCLUDED = "included" + CREATION_DATE = "creation_date" + CUSTOM_PROPERTIES = "custom_properties" + EMAIL = "email" + FIRST_NAME = "first_name" + LAST_NAME = "last_name" + ROLE = "role" + CUSTOM_ROLES = "custom_roles" + RULES = "rules" + ATTRIBUTE = "attribute" + OP = "op" + VALUES = "values" + VALUE_TYPE = "value_type" + NEGATE = "negate" + CLAUSES = "clauses" + WEIGHT = "weight" + BUCKET_BY = "bucket_by" + ROLLOUT_WEIGHTS = "rollout_weights" + VARIATION = "variation" + TARGETS = "targets" + PREREQUISITES = "prerequisites" + FLAG_KEY = "flag_key" + TRACK_EVENTS = "track_events" + FALLTHROUGH = "fallthrough" + KIND = "kind" + CONFIG = "config" + DEFAULT_ON_VARIATION = "default_on_variation" + DEFAULT_OFF_VARIATION = "default_off_variation" + DEFAULTS = "defaults" + ON_VARIATION = "on_variation" + OFF_VARIATION = "off_variation" + SERVICE_TOKEN = "service_token" + DEFAULT_API_VERSION = "default_api_version" + TOKEN = "token" + EXPIRE = "expire" + ID = "id" + CLIENT_SIDE_AVAILABILITY = "client_side_availability" + ARCHIVED = "archived" + APPROVAL_SETTINGS = "approval_settings" + REQUIRED = "required" + CAN_REVIEW_OWN_REQUEST = "can_review_own_request" + MIN_NUM_APPROVALS = "min_num_approvals" + CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" + REQUIRED_APPROVAL_TAGS = "required_approval_tags" ) diff --git a/launchdarkly/resource_launchdarkly_environment.go b/launchdarkly/resource_launchdarkly_environment.go index d4a0ad2b..c85125dc 100644 --- a/launchdarkly/resource_launchdarkly_environment.go +++ b/launchdarkly/resource_launchdarkly_environment.go @@ -65,6 +65,20 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro return fmt.Errorf("failed to create environment: [%+v] for project key: %s: %s", envPost, projectKey, handleLdapiErr(err)) } + approvalSettings := d.Get(APPROVAL_SETTINGS) + if len(approvalSettings.([]interface{})) > 0 { + err = resourceEnvironmentUpdate(d, metaRaw) + if err != nil { + // if there was a problem in the update state, we need to clean up completely by deleting the env + _, deleteErr := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key) + if deleteErr != nil { + return fmt.Errorf("failed to clean up environment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + } + return fmt.Errorf("failed to update environment with name %q key %q for projectKey %q: %s", + name, key, projectKey, handleLdapiErr(err)) + } + } + d.SetId(projectKey + "/" + key) return resourceEnvironmentRead(d, metaRaw) } @@ -96,7 +110,13 @@ func resourceEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) erro patchReplace("/confirmChanges", &confirmChanges), } - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { + oldApprovalSettings, newApprovalSettings := d.GetChange(APPROVAL_SETTINGS) + approvalPatch, err := approvalPatchFromSettings(oldApprovalSettings, newApprovalSettings) + if err != nil { + return err + } + patch = append(patch, approvalPatch...) + _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key, patch) }) diff --git a/launchdarkly/resource_launchdarkly_environment_test.go b/launchdarkly/resource_launchdarkly_environment_test.go index 7672c493..04f6cc1c 100644 --- a/launchdarkly/resource_launchdarkly_environment_test.go +++ b/launchdarkly/resource_launchdarkly_environment_test.go @@ -61,6 +61,43 @@ resource "launchdarkly_environment" "staging" { require_comments = false confirm_changes = true } +` + + testAccEnvironmentWithApprovals = ` +resource "launchdarkly_environment" "approvals_test" { + name = "Approvals Test" + key = "approvals-test" + color = "ababab" + project_key = launchdarkly_project.test.key + approval_settings { + can_review_own_request = false + min_num_approvals = 2 + required_approval_tags = ["approvals_required"] + } +} +` + testAccEnvironmentWithApprovalsUpdate = ` +resource "launchdarkly_environment" "approvals_test" { + name = "Approvals Test 2.0" + key = "approvals-test" + color = "bababa" + project_key = launchdarkly_project.test.key + approval_settings { + required = true + can_review_own_request = true + min_num_approvals = 1 + can_apply_declined_changes = true + } +} +` + + testAccEnvironmentWithApprovalsRemoved = ` +resource "launchdarkly_environment" "approvals_test" { + name = "Approvals Test 2.1" + key = "approvals-test" + color = "bababa" + project_key = launchdarkly_project.test.key +} ` ) @@ -220,6 +257,72 @@ func TestAccEnvironment_Invalid(t *testing.T) { }) } +func TestAccEnvironmentWithApprovals(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_environment.approvals_test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccEnvironmentWithApprovals), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test"), + resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), + resource.TestCheckResourceAttr(resourceName, "color", "ababab"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "false"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "false"), // should default to false + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.min_num_approvals", "2"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.required_approval_tags.0", "approvals_required"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: withRandomProject(projectKey, testAccEnvironmentWithApprovalsUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test 2.0"), + resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), + resource.TestCheckResourceAttr(resourceName, "color", "bababa"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.required", "true"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "true"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "true"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.min_num_approvals", "1"), + resource.TestCheckNoResourceAttr(resourceName, "approval_settings.0.required_approval_tags"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: withRandomProject(projectKey, testAccEnvironmentWithApprovalsRemoved), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test 2.1"), + resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), + resource.TestCheckResourceAttr(resourceName, "color", "bababa"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckNoResourceAttr(resourceName, "approval_settings"), + ), + }, + }, + }) +} + func testAccCheckEnvironmentExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index 5713cbc1..f3442a87 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -112,7 +112,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { return fmt.Errorf("failed to update project with key %q: %s", projectKey, handleLdapiErr(err)) } // Update environments if necessary - schemaEnvList := d.Get(ENVIRONMENTS) + oldSchemaEnvList, newSchemaEnvList := d.GetChange(ENVIRONMENTS) // Get the project so we can see if we need to create any environments or just update existing environments rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { return client.ld.ProjectsApi.GetProject(client.ctx, projectKey) @@ -122,7 +122,14 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { } project := rawProject.(ldapi.Project) - environmentConfigs := schemaEnvList.([]interface{}) + environmentConfigs := newSchemaEnvList.([]interface{}) + oldEnvironmentConfigs := oldSchemaEnvList.([]interface{}) + var oldEnvConfigsForCompare = make(map[string]map[string]interface{}, len(oldEnvironmentConfigs)) + for _, env := range oldEnvironmentConfigs { + envConfig := env.(map[string]interface{}) + envKey := envConfig[KEY].(string) + oldEnvConfigsForCompare[envKey] = envConfig + } // save envs in a key:config map so we can more easily figure out which need to be patchRemoved after var envConfigsForCompare = make(map[string]map[string]interface{}, len(environmentConfigs)) for _, env := range environmentConfigs { @@ -142,8 +149,15 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { } } + var oldEnvConfig map[string]interface{} + if rawOldConfig, ok := oldEnvConfigsForCompare[envKey]; ok { + oldEnvConfig = rawOldConfig + } // by default patching an env that was not recently tracked in the state will import it into the tf state - patches := getEnvironmentUpdatePatches(envConfig) + patches, err := getEnvironmentUpdatePatches(oldEnvConfig, envConfig) + if err != nil { + return err + } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey, patches) diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index b377d557..7254bec0 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -79,12 +79,46 @@ resource "launchdarkly_project" "env_test" { require_comments = true confirm_changes = true } + environments { + key = "new-approvals-env" + name = "New approvals environment" + color = "EEEEEE" + tags = ["new"] + approval_settings { + required = true + can_review_own_request = true + min_num_approvals = 2 + } + } +} +` + testAccProjectWithEnvironmentUpdateApprovalSettings = ` +resource "launchdarkly_project" "env_test" { + key = "%s" + name = "test project" + environments { + key = "test-env" + name = "test environment updated" + color = "AAAAAA" + tags = ["terraform", "test", "updated"] + default_ttl = 30 + secure_mode = true + default_track_events = true + require_comments = true + confirm_changes = true + } environments { - key = "new-env" - name = "New test environment" + key = "new-approvals-env" + name = "New approvals environment" color = "EEEEEE" tags = ["new"] + approval_settings { + required_approval_tags = ["approvals_required"] + can_review_own_request = false + min_num_approvals = 1 + can_apply_declined_changes = false + } } } ` @@ -243,7 +277,36 @@ func TestAccProject_WithEnvironments(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "environments.0.confirm_changes", "true"), // Check environment 1 is created - resource.TestCheckResourceAttr(resourceName, "environments.1.name", "New test environment"), + resource.TestCheckResourceAttr(resourceName, "environments.1.key", "new-approvals-env"), + resource.TestCheckResourceAttr(resourceName, "environments.1.name", "New approvals environment"), + resource.TestCheckResourceAttr(resourceName, "environments.1.tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "environments.1.color", "EEEEEE"), + resource.TestCheckResourceAttr(resourceName, "environments.1.default_ttl", "0"), + resource.TestCheckResourceAttr(resourceName, "environments.1.secure_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.default_track_events", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.require_comments", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.confirm_changes", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.required", "true"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_review_own_request", "true"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.min_num_approvals", "2"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_apply_declined_changes", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + }, + { + Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdateApprovalSettings, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, "environments.#", "2"), + + // Check approval_settings have updated as expected + resource.TestCheckResourceAttr(resourceName, "environments.1.key", "new-approvals-env"), + resource.TestCheckResourceAttr(resourceName, "environments.1.name", "New approvals environment"), resource.TestCheckResourceAttr(resourceName, "environments.1.tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "environments.1.color", "EEEEEE"), resource.TestCheckResourceAttr(resourceName, "environments.1.default_ttl", "0"), @@ -251,6 +314,11 @@ func TestAccProject_WithEnvironments(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "environments.1.default_track_events", "false"), resource.TestCheckResourceAttr(resourceName, "environments.1.require_comments", "false"), resource.TestCheckResourceAttr(resourceName, "environments.1.confirm_changes", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.required", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.required_approval_tags.0", "approvals_required"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_review_own_request", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.min_num_approvals", "1"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_apply_declined_changes", "false"), ), }, { diff --git a/website/docs/r/environment.html.markdown b/website/docs/r/environment.html.markdown index 6d9614a9..1a6b505f 100644 --- a/website/docs/r/environment.html.markdown +++ b/website/docs/r/environment.html.markdown @@ -26,6 +26,24 @@ resource "launchdarkly_environment" "staging" { } ``` +```hcl +resource "launchdarkly_environment" "approvals_example" { + name = "Approvals Example Environment" + key = "approvals-example" + color = "ff00ff" + tags = ["terraform", "staging"] + + approval_settings { + required = true + can_review_own_request = true + min_num_approvals = 2 + can_apply_declined_changes = true + } + + project_key = launchdarkly_project.example.key +} +``` + ## Argument Reference - `project_key` - (Required) - The environment's project key. @@ -48,6 +66,8 @@ resource "launchdarkly_environment" "staging" { - `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. This field will default to `false` when not set. +- `approval_settings` - (Optional) A nested block describing the environment approval settings. To learn more about this feature, read [Approvals](https://docs.launchdarkly.com/home/feature-workflows/approvals). To learn more about configuring them in Terraform, read [Nested Approval Settings Blocks](#nested-approval-settings-blocks). + ## Attribute Reference In addition to the arguments above, the resource exports the following attributes: @@ -60,6 +80,20 @@ In addition to the arguments above, the resource exports the following attribute - `client_side_id` - The environment's client-side ID. +### Nested Approval Settings Blocks + +Nested `approval_settings` blocks have the following structure: + +- `required` - Set to `true` for changes to flags in this environment to require approval. You may only set `required` to true if `required_approval_tags` is not set and vice versa. Defaults to `false`. + +- `can_review_own_request` - Set to `true` if requesters can approve or decline their own request. They may always comment. Defaults to `false`. + +- `min_num_approvals` - The number of approvals required before an approval request can be applied. This number must be between 1 and 5. Defaults to 1. + +- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `false`. + +- `required_approval_tags` - An array of tags used to specify which flags with those tags require approval. You may only set `required_approval_tags` if `required` is not set to `true` and vice versa. + ## Import You can import a LaunchDarkly environment using this format: `project_key/environment_key`. diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 8d073dac..142319c1 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -27,6 +27,12 @@ resource "launchdarkly_project" "example" { name = "Production" color = "EEEEEE" tags = ["terraform"] + approval_settings { + can_review_own_request = false + can_apply_declined_changes = false + min_num_approvals = 3 + required_approval_tags = ["approvals_required"] + } } environments { @@ -74,6 +80,22 @@ Nested `environments` blocks have the following structure: - `confirm_changes` - (Optional) Set to `true` if this environment requires confirmation for flag and segment changes. This field will default to `false` when not set. +- `approval_settings` - (Optional) A nested block describing the environment approval settings. To learn more about this feature, read [Approvals](https://docs.launchdarkly.com/home/feature-workflows/approvals). To learn more about configuring them in Terraform, read [Nested Environments Approval Settings Blocks](#nested-environments-approval-settings-blocks). + +### Nested Environments Approval Settings Blocks + +Nested environments `approval_settings` blocks have the following structure: + +- `required` - Set to `true` for changes to flags in this environment to require approval. You may only set `required` to true if `required_approval_tags` is not set and vice versa. Defaults to `false`. + +- `can_review_own_request` - Set to `true` if requesters can approve or decline their own request. They may always comment. Defaults to `false`. + +- `min_num_approvals` - The number of approvals required before an approval request can be applied. This number must be between 1 and 5. Defaults to 1. + +- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `false`. + +- `required_approval_tags` - An array of tags used to specify which flags with those tags require approval. You may only set `required_approval_tags` if `required` is not set to `true` and vice versa. + ## Import LaunchDarkly projects can be imported using the project's key, e.g. From 5ee5b57900ec4d90a3a8410f34eb5fc4bb669d72 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Mon, 11 Oct 2021 16:17:21 -0400 Subject: [PATCH 11/36] Imiller/sc 126337/make can apply declined changes default to (#158) * update error message * change default in schema * update tests * update docs' * update changelog --- CHANGELOG.md | 6 ++++++ launchdarkly/approvals_helper.go | 4 ++-- launchdarkly/resource_launchdarkly_environment_test.go | 6 +++--- launchdarkly/resource_launchdarkly_project_test.go | 2 +- launchdarkly/variations_helper.go | 2 +- website/docs/r/environment.html.markdown | 2 +- website/docs/r/project.html.markdown | 2 +- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c3e641..61562492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +BUG FIXES: + +- Fixed an oversight in the approval settings where `can_apply_declined_changes` was defaulting to `false` where it should have been defaulting to `true` in alignment with the LaunchDarkly API. + +- Updated an error message. + ## [2.1.0] (October 8, 2021) FEATURES: diff --git a/launchdarkly/approvals_helper.go b/launchdarkly/approvals_helper.go index e4120970..e8f888c1 100644 --- a/launchdarkly/approvals_helper.go +++ b/launchdarkly/approvals_helper.go @@ -37,8 +37,8 @@ func approvalSchema() *schema.Schema { CAN_APPLY_DECLINED_CHANGES: { Type: schema.TypeBool, Optional: true, - Description: "Whether changes can be applied as long as minNumApprovals is met, regardless of whether any reviewers have declined a request.", - Default: false, + Description: "Whether changes can be applied as long as minNumApprovals is met, regardless of whether any reviewers have declined a request. Defaults to true", + Default: true, }, REQUIRED_APPROVAL_TAGS: { Type: schema.TypeList, diff --git a/launchdarkly/resource_launchdarkly_environment_test.go b/launchdarkly/resource_launchdarkly_environment_test.go index 04f6cc1c..20afd84c 100644 --- a/launchdarkly/resource_launchdarkly_environment_test.go +++ b/launchdarkly/resource_launchdarkly_environment_test.go @@ -86,7 +86,7 @@ resource "launchdarkly_environment" "approvals_test" { required = true can_review_own_request = true min_num_approvals = 1 - can_apply_declined_changes = true + can_apply_declined_changes = false } } ` @@ -276,7 +276,7 @@ func TestAccEnvironmentWithApprovals(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "color", "ababab"), resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "false"), - resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "false"), // should default to false + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "true"), // should default to true resource.TestCheckResourceAttr(resourceName, "approval_settings.0.min_num_approvals", "2"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.required_approval_tags.0", "approvals_required"), ), @@ -297,7 +297,7 @@ func TestAccEnvironmentWithApprovals(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.required", "true"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "true"), - resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "true"), + resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "false"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.min_num_approvals", "1"), resource.TestCheckNoResourceAttr(resourceName, "approval_settings.0.required_approval_tags"), ), diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index 7254bec0..5daceaff 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -289,7 +289,7 @@ func TestAccProject_WithEnvironments(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.required", "true"), resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_review_own_request", "true"), resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.min_num_approvals", "2"), - resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_apply_declined_changes", "false"), + resource.TestCheckResourceAttr(resourceName, "environments.1.approval_settings.0.can_apply_declined_changes", "true"), // defaults to true ), }, { diff --git a/launchdarkly/variations_helper.go b/launchdarkly/variations_helper.go index 59d03c5d..f5d9c75e 100644 --- a/launchdarkly/variations_helper.go +++ b/launchdarkly/variations_helper.go @@ -74,7 +74,7 @@ func validateVariationType(val interface{}, key string) (warns []string, errs [] case BOOL_VARIATION, STRING_VARIATION, NUMBER_VARIATION, JSON_VARIATION: break default: - errs = append(errs, fmt.Errorf("%q contains an invalid value %q. Valid values are `boolean` and `string`", key, value)) + errs = append(errs, fmt.Errorf("%q contains an invalid value %q. Valid values are `boolean`, `string`, `number`, and `json`", key, value)) } return warns, errs } diff --git a/website/docs/r/environment.html.markdown b/website/docs/r/environment.html.markdown index 1a6b505f..1d6782e6 100644 --- a/website/docs/r/environment.html.markdown +++ b/website/docs/r/environment.html.markdown @@ -90,7 +90,7 @@ Nested `approval_settings` blocks have the following structure: - `min_num_approvals` - The number of approvals required before an approval request can be applied. This number must be between 1 and 5. Defaults to 1. -- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `false`. +- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `true`. - `required_approval_tags` - An array of tags used to specify which flags with those tags require approval. You may only set `required_approval_tags` if `required` is not set to `true` and vice versa. diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 142319c1..9c896d0e 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -92,7 +92,7 @@ Nested environments `approval_settings` blocks have the following structure: - `min_num_approvals` - The number of approvals required before an approval request can be applied. This number must be between 1 and 5. Defaults to 1. -- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `false`. +- `can_apply_declined_changes` - Set to `true` if changes can be applied as long as the `min_num_approvals` is met, regardless of whether any reviewers have declined a request. Defaults to `true`. - `required_approval_tags` - An array of tags used to specify which flags with those tags require approval. You may only set `required_approval_tags` if `required` is not set to `true` and vice versa. From 9b2a2880cee4486b7b8288960d83ae35bf0f30d9 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Fri, 29 Oct 2021 14:51:12 +0200 Subject: [PATCH 12/36] clarify rollout weights (#159) --- website/docs/r/feature_flag_environment.html.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/r/feature_flag_environment.html.markdown b/website/docs/r/feature_flag_environment.html.markdown index 59a0e4cd..6a907177 100644 --- a/website/docs/r/feature_flag_environment.html.markdown +++ b/website/docs/r/feature_flag_environment.html.markdown @@ -99,7 +99,7 @@ The nested `fallthrough` (previously `flag_fallthrough`) block has the following - `variation` - (Optional) The default integer variation index to serve if no `prerequisites`, `target`, or `rules` apply. You must specify either `variation` or `rollout_weights`. -- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if no `prerequisites`, `target`, or `rules` apply. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. +- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if no `prerequisites`, `target`, or `rules` apply. The sum of the `rollout_weights` must equal 100000 and the number of rollout weights specified in the array must match the number of flag variations. You must specify either `variation` or `rollout_weights`. - `bucket_by` - (Optional) Group percentage rollout by a custom attribute. This argument is only valid if `rollout_weights` is also specified. @@ -111,7 +111,7 @@ Nested `rules` blocks have the following structure: - `variation` - (Optional) The integer variation index to serve if the rule clauses evaluate to `true`. You must specify either `variation` or `rollout_weights`. -- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. +- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000 and the number of rollout weights specified in the array must match the number of flag variations. You must specify either `variation` or `rollout_weights`. - `bucket_by` - (Optional) Group percentage rollout by a custom attribute. This argument is only valid if `rollout_weights` is also specified. @@ -133,7 +133,7 @@ Nested `fallthrough` blocks have the following structure: - `variation` - (Optional) The integer variation index to serve if the rule clauses evaluate to `true`. You must specify either `variation` or `rollout_weights`. -- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000. You must specify either `variation` or `rollout_weights`. +- `rollout_weights` - (Optional) List of integer percentage rollout weights (in thousandths of a percent) to apply to each variation if the rule clauses evaluates to `true`. The sum of the `rollout_weights` must equal 100000 and the number of rollout weights specified in the array must match the number of flag variations. You must specify either `variation` or `rollout_weights`. ## Attributes Reference From aabe026cc66bf29ebf018aa508e9edd837da1484 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Thu, 23 Dec 2021 14:50:02 +0100 Subject: [PATCH 13/36] Upgrade Terraform provider to use api-client-go 7 (#160) * Start updating go client * Continue updating resources * feature flag resource * feature flag resource test * approval settings and custom role policies * destination resource * environment * feature flag environment * project * segment * team member * webhook * rule, segment rule, segments, and team member helpers * variations helper * data source tests * webhooks helper * resource tests * variations helper test * policy helpers and tests probably will all fail * custom role and access token probably also broken * policy statements helper update * instantiate config prior to setting other values on it * fix 401 * vendor v7 * update all imports to v7 * missed one * fix nil pointer dereference * instantiate with var instead of make to fix non-empty slice bu * custom roles now complains if you try to set more than one of Role, InlineRole, or CustomRoleIds * fix webhook statemetns * webhook read check if statements are nil & webhook create only set secret if defined * update changelog * fix webhook data source tests * new pointers in client FeatureFlagConfig breaking ff env data source tests * fix negative expiry thing (apparently the api no longer handles it so we have to) * move reset into helper function * hackily handle token reset requests until client bug is fixed * fix variation transformation - can no longer point to empty string * fix notResource and notActions * fix policy helper tests * fix the weird empty string value in variations issue * strip protocol from host if provided * go mod tidy && vendor * update all go packages * clean up commentsg * clean up comment * Ffeldberg/sc 130551/add the ability to adjust the sdks using (#161) * chore: first attempts * feat: updating flag resources works, reading/creating plan is a bit iffy * chore: clean up comments a bit * chore: add TODO as reminder * chore: add more todos * chore: use ConflictsWith to ensure users dont use both includeInSnippet and clientSideAvailablity * chore: pr feedback - update deprecation message, always read both sdk client values * feat: set both includeInSnippet and clientSideAvailability to computed properties * wip: attempt to use computed and update/create logic blocks * add comment * use getOk instead of getOkExists for nested, rename vars * wip: use getOk instead of getOkExists for nested, test modified read * temp: temporary test changes for local debugging * chore: add some comments * feat: working apart from switching back to deprecated option in edge case * chore: remove commented out code * fix: fix issue with read * test: reset tests * fix: fix typo in read * fix: account for datasources in featureflag read * test: add tests for clientSideAvailability changes * docs: update example docs * docs: add include_in_snippet to website docs and add deprecation notice * docs: update docs * chore: update CHANGELOG * fix: fix most issues caused by go client v7 merge * fix: fix other merge issues * chore: update CHANGELOG * chore: apply codereview keys suggestions Co-authored-by: Isabelle Miller * chore: Update launchdarkly/feature_flags_helper.go keys Co-authored-by: Isabelle Miller * chore: update keys.go to reflect clientsideavailablity keys * chore: go fmt * test: add tests for switching between CSA and IIS, clean up * chore: go fmt * test: remove pure create tests * chore: add TODOs where required * test: add new test case for reverting to default CSA * feat: set clientSideAvailability according to project defaults for flags * test: update tests accordingly and add comments * chore: fmt * refactor project default get into its own function * test: account for default project CSA settings * feat: basic working implementation of resetting to defaults using customizeDiff * feat: attempted changes to customdiff to support CSA * chore: clean up flag resource read * chore: clean up comments and todos * docs: update docs to mention project level defaults for flag sdk settings * chore: remove prints to clean up test logs * chore: go mod vendor * chore: Update CHANGELOG.md Co-authored-by: Isabelle Miller * chore: address pr feedback * docs: update changelog Co-authored-by: Isabelle Miller Co-authored-by: Henry Barrow Co-authored-by: Fabian --- CHANGELOG.md | 13 + examples/v2/feature_flags/README.md | 35 +- go.mod | 26 +- go.sum | 189 ++++++++--- launchdarkly/account_cleaner_test.go | 114 ------- launchdarkly/approvals_helper.go | 14 +- launchdarkly/clause_helper.go | 2 +- launchdarkly/clause_helper_test.go | 2 +- launchdarkly/config.go | 47 +-- launchdarkly/custom_properties_helper.go | 2 +- launchdarkly/custom_properties_helper_test.go | 2 +- ...ta_source_launchdarkly_environment_test.go | 18 +- .../data_source_launchdarkly_feature_flag.go | 16 - ...nchdarkly_feature_flag_environment_test.go | 19 +- ...a_source_launchdarkly_feature_flag_test.go | 14 +- .../data_source_launchdarkly_project_test.go | 18 +- .../data_source_launchdarkly_segment_test.go | 26 +- .../data_source_launchdarkly_team_member.go | 16 +- ...ta_source_launchdarkly_team_member_test.go | 20 +- .../data_source_launchdarkly_webhook_test.go | 17 +- launchdarkly/default_variations_helper.go | 2 +- .../default_variations_helper_test.go | 2 +- launchdarkly/environments_helper.go | 6 +- launchdarkly/environments_helper_test.go | 4 +- launchdarkly/fallthrough_helper.go | 8 +- .../feature_flag_environment_helper.go | 9 +- launchdarkly/feature_flags_helper.go | 65 +++- launchdarkly/helper.go | 6 +- launchdarkly/keys.go | 2 + launchdarkly/policies_helper.go | 12 +- launchdarkly/policy_statements_helper.go | 74 +++-- launchdarkly/policy_statements_helper_test.go | 34 +- launchdarkly/prerequisite_helper.go | 2 +- launchdarkly/project_helper.go | 4 +- launchdarkly/provider.go | 8 +- .../resource_launchdarkly_access_token.go | 138 +++++--- ...resource_launchdarkly_access_token_test.go | 2 +- .../resource_launchdarkly_custom_role.go | 33 +- .../resource_launchdarkly_custom_role_test.go | 4 +- .../resource_launchdarkly_destination.go | 26 +- .../resource_launchdarkly_destination_test.go | 2 +- .../resource_launchdarkly_environment.go | 26 +- .../resource_launchdarkly_environment_test.go | 2 +- .../resource_launchdarkly_feature_flag.go | 139 +++++++- ...e_launchdarkly_feature_flag_environment.go | 25 +- ...nchdarkly_feature_flag_environment_test.go | 5 +- ...resource_launchdarkly_feature_flag_test.go | 309 +++++++++++++++++- launchdarkly/resource_launchdarkly_project.go | 24 +- .../resource_launchdarkly_project_test.go | 2 +- launchdarkly/resource_launchdarkly_segment.go | 37 ++- .../resource_launchdarkly_segment_test.go | 2 +- .../resource_launchdarkly_team_member.go | 22 +- .../resource_launchdarkly_team_member_test.go | 2 +- launchdarkly/resource_launchdarkly_webhook.go | 33 +- .../resource_launchdarkly_webhook_test.go | 2 +- launchdarkly/rollout_helper.go | 2 +- launchdarkly/rule_helper.go | 4 +- launchdarkly/segment_rule_helper.go | 8 +- launchdarkly/segments_helper.go | 4 +- launchdarkly/target_helper.go | 2 +- launchdarkly/target_helper_test.go | 2 +- launchdarkly/team_member_helper.go | 6 +- launchdarkly/test_utils.go | 12 +- launchdarkly/variations_helper.go | 81 +++-- launchdarkly/variations_helper_test.go | 8 +- launchdarkly/webhooks_helper.go | 17 +- website/docs/d/feature_flag.html.markdown | 2 + website/docs/r/feature_flag.html.markdown | 13 +- 68 files changed, 1208 insertions(+), 636 deletions(-) delete mode 100644 launchdarkly/account_cleaner_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c85bd21..544944cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ## [Unreleased] +ENHANCEMENTS: + +- Upgraded the LaunchDarkly API client to version 7. +- Flag resource creation respects project level SDK availability defaults. + +FEATURES: + +- Added `client_side_availability` block to the `launchdarkly_feature_flag` resource to allow setting whether this flag should be made available to the client-side JavaScript SDK using the client-side ID, mobile key, or both. + +NOTES: + +- The `launchdarkly_feature_flag` resource's argument `include_in_snippet` has been deprecated in favor of `client_side_availability`. Please update your config to use `client_side_availability` in order to maintain compatibility with future versions. + ## [2.1.1] (October 11, 2021) BUG FIXES: diff --git a/examples/v2/feature_flags/README.md b/examples/v2/feature_flags/README.md index d11156c9..b71fafff 100644 --- a/examples/v2/feature_flags/README.md +++ b/examples/v2/feature_flags/README.md @@ -25,7 +25,7 @@ Terraform will perform the following actions: + resource "launchdarkly_feature_flag" "boolean_flag" { + description = "An example boolean feature flag that can be turned either on or off" + id = (known after apply) - + include_in_snippet = false + + include_in_snippet = (known after apply) + key = "boolean-flag" + maintainer_id = (known after apply) + name = "Bool feature flag" @@ -33,6 +33,11 @@ Terraform will perform the following actions: + temporary = false + variation_type = "boolean" + + client_side_availability { + + using_environment_id = (known after apply) + + using_mobile_key = (known after apply) + } + + defaults { + off_variation = (known after apply) + on_variation = (known after apply) @@ -49,7 +54,7 @@ Terraform will perform the following actions: + resource "launchdarkly_feature_flag" "json_flag" { + description = "An example of a multivariate feature flag with JSON variations" + id = (known after apply) - + include_in_snippet = false + + include_in_snippet = (known after apply) + key = "json-flag" + maintainer_id = (known after apply) + name = "JSON-based feature flag" @@ -60,6 +65,11 @@ Terraform will perform the following actions: + temporary = false + variation_type = "json" + + client_side_availability { + + using_environment_id = (known after apply) + + using_mobile_key = (known after apply) + } + + defaults { + off_variation = (known after apply) + on_variation = (known after apply) @@ -88,7 +98,7 @@ Terraform will perform the following actions: + resource "launchdarkly_feature_flag" "number_flag" { + description = "An example of a multivariate feature flag with numeric variations" + id = (known after apply) - + include_in_snippet = false + + include_in_snippet = (known after apply) + key = "number-flag" + maintainer_id = (known after apply) + name = "Number value-based feature flag" @@ -99,6 +109,11 @@ Terraform will perform the following actions: + temporary = false + variation_type = "number" + + client_side_availability { + + using_environment_id = (known after apply) + + using_mobile_key = (known after apply) + } + + defaults { + off_variation = (known after apply) + on_variation = (known after apply) @@ -122,7 +137,7 @@ Terraform will perform the following actions: + resource "launchdarkly_feature_flag" "string_flag" { + description = "An example of a multivariate feature flag with string variations" + id = (known after apply) - + include_in_snippet = false + + include_in_snippet = (known after apply) + key = "string-flag" + maintainer_id = (known after apply) + name = "String-based feature flag" @@ -133,6 +148,11 @@ Terraform will perform the following actions: + temporary = false + variation_type = "string" + + client_side_availability { + + using_environment_id = (known after apply) + + using_mobile_key = (known after apply) + } + + defaults { + off_variation = (known after apply) + on_variation = (known after apply) @@ -246,13 +266,18 @@ Terraform will perform the following actions: # launchdarkly_project.tf_flag_examples will be created + resource "launchdarkly_project" "tf_flag_examples" { + id = (known after apply) - + include_in_snippet = false + + include_in_snippet = (known after apply) + key = "tf-flag-examples" + name = "Terraform Project for Flag Examples" + tags = [ + "terraform-managed", ] + + client_side_availability { + + using_environment_id = (known after apply) + + using_mobile_key = (known after apply) + } + + environments { + api_key = (sensitive value) + client_side_id = (sensitive value) diff --git a/go.mod b/go.mod index 30f4c4b5..2842d7e6 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,28 @@ module github.com/launchdarkly/terraform-provider-launchdarkly go 1.16 require ( - github.com/antihax/optional v1.0.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 - github.com/launchdarkly/api-client-go v5.3.0+incompatible + github.com/agext/levenshtein v1.2.3 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect + github.com/hashicorp/go-hclog v1.0.0 // indirect + github.com/hashicorp/go-plugin v1.4.3 // indirect + github.com/hashicorp/hcl/v2 v2.11.1 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 + github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 // indirect + github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect + github.com/launchdarkly/api-client-go/v7 v7.0.0 + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/stretchr/testify v1.7.0 + github.com/zclconf/go-cty v1.10.0 // indirect + golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect + google.golang.org/grpc v1.42.0 // indirect ) diff --git a/go.sum b/go.sum index 167faefd..fe8b8993 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,10 @@ cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6 cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.61.0 h1:NLQf5e1OMspfNT1RAHOB3ublr1TW3YTXO8OiWwVjK2U= cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -40,16 +42,17 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= @@ -70,12 +73,20 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -85,10 +96,15 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= @@ -114,6 +130,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -127,9 +144,11 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -138,12 +157,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -157,11 +175,14 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= @@ -170,38 +191,53 @@ github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= -github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= -github.com/hashicorp/go-plugin v1.4.1 h1:6UltRQlLN9iZO513VveELp5xyaFxVD2+1OVylE+2E+w= github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggUE= +github.com/hashicorp/hc-install v0.3.1 h1:VIjllE6KyAI1A244G8kTaHXy+TL5/XYzvrtFi8po/Yk= +github.com/hashicorp/hc-install v0.3.1/go.mod h1:3LCdWcCDS1gaHC9mhHCGbkYfoY6vdsKohGjugbZdZak= github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8= +github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc= +github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.14.0 h1:UQoUcxKTZZXhyyK68Cwn4mApT4mnFPmEXPiqaHL9r+w= -github.com/hashicorp/terraform-exec v0.14.0/go.mod h1:qrAASDq28KZiMPDnQ02sFS9udcqEkRly002EA2izXTA= -github.com/hashicorp/terraform-json v0.12.0 h1:8czPgEEWWPROStjkWPUnTQDXmpmZPlkQAwYYLETaTvw= -github.com/hashicorp/terraform-json v0.12.0/go.mod h1:pmbq9o4EuL43db5+0ogX10Yofv1nozM+wskr/bGFJpI= -github.com/hashicorp/terraform-plugin-go v0.3.0 h1:AJqYzP52JFYl9NABRI7smXI1pNjgR5Q/y2WyVJ/BOZA= -github.com/hashicorp/terraform-plugin-go v0.3.0/go.mod h1:dFHsQMaTLpON2gWhVWT96fvtlc/MF1vSy3OdMhWBzdM= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 h1:SuI59MqNjYDrL7EfqHX9V6P/24isgqYx/FdglwVs9bg= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0/go.mod h1:grseeRo9g3yNkYW09iFlV8LG78jTa1ssBgouogQg/RU= +github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E= +github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I= +github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY= +github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= +github.com/hashicorp/terraform-plugin-go v0.5.0 h1:+gCDdF0hcYCm0YBTxrP4+K1NGIS5ZKZBKDORBewLJmg= +github.com/hashicorp/terraform-plugin-go v0.5.0/go.mod h1:PAVN26PNGpkkmsvva1qfriae5Arky3xl3NfzKa8XFVM= +github.com/hashicorp/terraform-plugin-log v0.2.0 h1:rjflRuBqCnSk3UHOR25MP1G5BDLKktTA6lNjjcAnBfI= +github.com/hashicorp/terraform-plugin-log v0.2.0/go.mod h1:E1kJmapEHzqu1x6M++gjvhzM2yMQNXPVWZRCB8sgYjg= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 h1:osXVmeDNoYGxPGnIFxrR//rxa47XIMwzOBL9/rX0iDM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0/go.mod h1:FjM9DXWfP0w/AeOtJoSKHBZ01LqmaO6uP4bXhv3fekw= +github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= +github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 h1:R/I8ofvXuPcTNoc//N4ruvaHGZcShI/VuU2iXo875Lo= +github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045/go.mod h1:anRyJbe12BZscpFgaeGu9gH12qfdBP094LYFtuAFzd4= +github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= +github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -234,17 +270,21 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/launchdarkly/api-client-go v5.3.0+incompatible h1:xB4QGNbNzAUm2UDdtlsHhmCrs6l7OQGWFgs1xB6s/u8= -github.com/launchdarkly/api-client-go v5.3.0+incompatible/go.mod h1:INGa7NUZYSwVozwPV7l6ikgD7pzSOpZvg9I5sqCZIWs= +github.com/launchdarkly/api-client-go/v7 v7.0.0 h1:mCVGV3adts81Gtq2YxwCi6lvS/V9hYGJlqilLGFKj98= +github.com/launchdarkly/api-client-go/v7 v7.0.0/go.mod h1:5FlSAYTMrNa4UOiuSSL1+85NOiJel6cZT2P86ihNR9s= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -255,28 +295,31 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.4 h1:ZU1VNC02qyufSZsjjs7+khruk2fKvbQ3TwRV/IBCeFA= -github.com/mitchellh/go-testing-interface v1.0.4/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -284,12 +327,14 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -305,10 +350,12 @@ github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6e github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.8.4 h1:pwhhz5P+Fjxse7S7UriBrMu6AUJSZM5pKqGem1PjGAs= -github.com/zclconf/go-cty v1.8.4/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= +github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -316,6 +363,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -325,8 +373,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -373,6 +422,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -386,15 +436,21 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b h1:MWaHNqZy3KTpuTMAGvv+Kw+ylsEpmyJZizz1dqxnu28= +golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -421,6 +477,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -432,11 +489,19 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -444,8 +509,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -486,12 +553,16 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed h1:+qzWo37K31KxduIYaBeMqJ8MUOyTayOQKpH9aDPLMSY= golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -506,15 +577,17 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -539,11 +612,16 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200711021454-869866162049 h1:YFTFpQhgvrLrmxtiIncJxFXeCyq84ixuKWVCaCAi9Oc= google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -556,8 +634,13 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -567,8 +650,11 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -580,6 +666,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/launchdarkly/account_cleaner_test.go b/launchdarkly/account_cleaner_test.go deleted file mode 100644 index d74eb769..00000000 --- a/launchdarkly/account_cleaner_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package launchdarkly - -import ( - "fmt" - "os" - "testing" - "time" - - ldapi "github.com/launchdarkly/api-client-go" - "github.com/stretchr/testify/require" -) - -const ( - dummyProject = "dummy-project" -) - -func TestCleanAccount(t *testing.T) { - // Uncomment this if you really want to wipe the account. - t.SkipNow() - - fmt.Println("****** DANGER!!!! ******") - fmt.Println("We're about to clean your account!!! pausing 10 seconds so you can kill this in case it was run by mistake!!") - time.Sleep(10 * time.Second) - - require.NoError(t, cleanAccount()) -} - -func cleanAccount() error { - c, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) - if err != nil { - return err - } - - err = c.cleanProjects() - if err != nil { - return err - } - err = c.cleanTeamMembers() - if err != nil { - return err - } - - err = c.cleanCustomRoles() - if err != nil { - return err - } - return nil -} - -// cleanProjects ensures exactly one project with name and key 'dummy-project' exists for an account. -// LD requires at least one project in an account. -func (c *Client) cleanProjects() error { - // make sure we have a dummy project - _, response, err := c.ld.ProjectsApi.GetProject(c.ctx, dummyProject) - - if response.StatusCode == 404 { - _, _, err = c.ld.ProjectsApi.PostProject(c.ctx, ldapi.ProjectBody{Name: dummyProject, Key: dummyProject}) - if err != nil { - return handleLdapiErr(err) - } - } else { - if err != nil { - return err - } - } - projects, _, err := c.ld.ProjectsApi.GetProjects(c.ctx) - if err != nil { - return handleLdapiErr(err) - } - - // delete all but dummy project - for _, p := range projects.Items { - if p.Key != dummyProject { - _, err := c.ld.ProjectsApi.DeleteProject(c.ctx, p.Key) - if err != nil { - return handleLdapiErr(err) - } - } - } - return nil -} - -// cleanTeamMembers ensures the only team member is the account owner -func (c *Client) cleanTeamMembers() error { - members, _, err := c.ld.TeamMembersApi.GetMembers(c.ctx, &ldapi.TeamMembersApiGetMembersOpts{}) - if err != nil { - return handleLdapiErr(err) - } - for _, m := range members.Items { - if *m.Role != ldapi.OWNER_Role && m.PendingInvite == true { - _, err := c.ld.TeamMembersApi.DeleteMember(c.ctx, m.Id) - if err != nil { - return handleLdapiErr(err) - } - } - } - return nil -} - -// cleanCustomRoles deletes all custom roles -func (c *Client) cleanCustomRoles() error { - roles, _, err := c.ld.CustomRolesApi.GetCustomRoles(c.ctx) - if err != nil { - return handleLdapiErr(err) - } - - for _, r := range roles.Items { - _, err := c.ld.CustomRolesApi.DeleteCustomRole(c.ctx, r.Id) - if err != nil { - return handleLdapiErr(err) - } - } - return nil -} diff --git a/launchdarkly/approvals_helper.go b/launchdarkly/approvals_helper.go index e8f888c1..42197ada 100644 --- a/launchdarkly/approvals_helper.go +++ b/launchdarkly/approvals_helper.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func approvalSchema() *schema.Schema { @@ -54,15 +54,15 @@ func approvalSchema() *schema.Schema { } } -func approvalSettingsFromResourceData(val interface{}) (ldapi.EnvironmentApprovalSettings, error) { +func approvalSettingsFromResourceData(val interface{}) (ldapi.ApprovalSettings, error) { raw := val.([]interface{}) if len(raw) == 0 { - return ldapi.EnvironmentApprovalSettings{}, nil + return ldapi.ApprovalSettings{}, nil } approvalSettingsMap := raw[0].(map[string]interface{}) - settings := ldapi.EnvironmentApprovalSettings{ + settings := ldapi.ApprovalSettings{ CanReviewOwnRequest: approvalSettingsMap[CAN_REVIEW_OWN_REQUEST].(bool), - MinNumApprovals: int64(approvalSettingsMap[MIN_NUM_APPROVALS].(int)), + MinNumApprovals: int32(approvalSettingsMap[MIN_NUM_APPROVALS].(int)), CanApplyDeclinedChanges: approvalSettingsMap[CAN_APPLY_DECLINED_CHANGES].(bool), } // Required and RequiredApprovalTags should never be defined simultaneously @@ -72,7 +72,7 @@ func approvalSettingsFromResourceData(val interface{}) (ldapi.EnvironmentApprova tags := approvalSettingsMap[REQUIRED_APPROVAL_TAGS].([]interface{}) if len(tags) > 0 { if required { - return ldapi.EnvironmentApprovalSettings{}, fmt.Errorf("invalid approval_settings config: required and required_approval_tags cannot be set simultaneously") + return ldapi.ApprovalSettings{}, fmt.Errorf("invalid approval_settings config: required and required_approval_tags cannot be set simultaneously") } stringTags := make([]string, len(tags)) for i := range tags { @@ -85,7 +85,7 @@ func approvalSettingsFromResourceData(val interface{}) (ldapi.EnvironmentApprova return settings, nil } -func approvalSettingsToResourceData(settings ldapi.EnvironmentApprovalSettings) interface{} { +func approvalSettingsToResourceData(settings ldapi.ApprovalSettings) interface{} { transformed := map[string]interface{}{ CAN_REVIEW_OWN_REQUEST: settings.CanReviewOwnRequest, MIN_NUM_APPROVALS: settings.MinNumApprovals, diff --git a/launchdarkly/clause_helper.go b/launchdarkly/clause_helper.go index 8bd19762..a128d2b3 100644 --- a/launchdarkly/clause_helper.go +++ b/launchdarkly/clause_helper.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) const ( diff --git a/launchdarkly/clause_helper_test.go b/launchdarkly/clause_helper_test.go index d7bdd60d..ec9ec026 100644 --- a/launchdarkly/clause_helper_test.go +++ b/launchdarkly/clause_helper_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/launchdarkly/config.go b/launchdarkly/config.go index e24fca80..a69c0f9b 100644 --- a/launchdarkly/config.go +++ b/launchdarkly/config.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "net/http" + "time" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // The version string gets updated at build time using -ldflags @@ -17,40 +19,43 @@ const ( // Client is used by the provider to access the ld API. type Client struct { - apiKey string - apiHost string - ld *ldapi.APIClient - ctx context.Context + apiKey string + apiHost string + ld *ldapi.APIClient + ctx context.Context + fallbackClient *http.Client } func newClient(token string, apiHost string, oauth bool) (*Client, error) { if token == "" { return nil, errors.New("token cannot be empty") } - basePath := "https://app.launchdarkly.com/api/v2" - if apiHost != "" { - basePath = fmt.Sprintf("%s/api/v2", apiHost) - } - cfg := &ldapi.Configuration{ - BasePath: basePath, - DefaultHeader: make(map[string]string), - UserAgent: fmt.Sprintf("launchdarkly-terraform-provider/%s", version), - } + cfg := ldapi.NewConfiguration() + cfg.Host = apiHost + cfg.DefaultHeader = make(map[string]string) + cfg.UserAgent = fmt.Sprintf("launchdarkly-terraform-provider/%s", version) cfg.AddDefaultHeader("LD-API-Version", APIVersion) - ctx := context.WithValue(context.Background(), ldapi.ContextAPIKey, ldapi.APIKey{ - Key: token, - }) + ctx := context.WithValue(context.Background(), ldapi.ContextAPIKeys, map[string]ldapi.APIKey{ + "ApiKey": { + Key: token, + }}) if oauth { ctx = context.WithValue(context.Background(), ldapi.ContextAccessToken, token) } + // TODO: remove this once we get the go client reset endpoint fixed + fallbackClient := http.Client{ + Timeout: time.Duration(5 * time.Second), + } + return &Client{ - apiKey: token, - apiHost: apiHost, - ld: ldapi.NewAPIClient(cfg), - ctx: ctx, + apiKey: token, + apiHost: apiHost, + ld: ldapi.NewAPIClient(cfg), + ctx: ctx, + fallbackClient: &fallbackClient, }, nil } diff --git a/launchdarkly/custom_properties_helper.go b/launchdarkly/custom_properties_helper.go index 6e251f77..1abbc32f 100644 --- a/launchdarkly/custom_properties_helper.go +++ b/launchdarkly/custom_properties_helper.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // https://docs.launchdarkly.com/docs/custom-properties diff --git a/launchdarkly/custom_properties_helper_test.go b/launchdarkly/custom_properties_helper_test.go index 6a3cc38d..db6eaa3f 100644 --- a/launchdarkly/custom_properties_helper_test.go +++ b/launchdarkly/custom_properties_helper_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) diff --git a/launchdarkly/data_source_launchdarkly_environment_test.go b/launchdarkly/data_source_launchdarkly_environment_test.go index 770042d7..ece7017f 100644 --- a/launchdarkly/data_source_launchdarkly_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_environment_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -25,12 +25,10 @@ data "launchdarkly_environment" "test" { // for environment data source tests func testAccDataSourceEnvironmentScaffold(client *Client, projectKey string, envBody ldapi.EnvironmentPost) (*ldapi.Environment, error) { // create project - projectBody := ldapi.ProjectBody{ - Name: "Env Test Project", - Key: projectKey, - Environments: []ldapi.EnvironmentPost{ - envBody, - }, + projectBody := ldapi.ProjectPost{ + Name: "Env Test Project", + Key: projectKey, + Environments: &[]ldapi.EnvironmentPost{envBody}, } project, err := testAccDataSourceProjectCreate(client, projectBody) if err != nil { @@ -52,7 +50,7 @@ func TestAccDataSourceEnvironment_noMatchReturnsError(t *testing.T) { client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) require.NoError(t, err) projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: "Terraform Env Test Project", Key: projectKey, } @@ -96,8 +94,8 @@ func TestAccDataSourceEnv_exists(t *testing.T) { Name: envName, Key: envKey, Color: envColor, - SecureMode: true, - Tags: []string{ + SecureMode: ldapi.PtrBool(true), + Tags: &[]string{ "some", "tag", }, } diff --git a/launchdarkly/data_source_launchdarkly_feature_flag.go b/launchdarkly/data_source_launchdarkly_feature_flag.go index 8971a9c2..66c4bac2 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag.go @@ -19,22 +19,6 @@ func dataSourceFeatureFlag() *schema.Resource { Description: fmt.Sprintf("The uniform type for all variations. Can be either %q, %q, %q, or %q.", BOOL_VARIATION, STRING_VARIATION, NUMBER_VARIATION, JSON_VARIATION), } - schemaMap[CLIENT_SIDE_AVAILABILITY] = &schema.Schema{ - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "using_environment_id": { - Type: schema.TypeBool, - Optional: true, - }, - "using_mobile_key": { - Type: schema.TypeBool, - Optional: true, - }, - }, - }, - } return &schema.Resource{ Read: dataSourceFeatureFlagRead, Schema: schemaMap, diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go index 525e7338..b5ce2f78 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,7 +27,7 @@ func testAccDataSourceFeatureFlagEnvironmentScaffold(client *Client, projectKey, flagBody := ldapi.FeatureFlagBody{ Name: "Feature Flag Env Data Source Test", Key: flagKey, - Variations: []ldapi.Variation{ + Variations: &[]ldapi.Variation{ {Value: intfPtr(true)}, {Value: intfPtr(false)}, }, @@ -38,13 +38,12 @@ func testAccDataSourceFeatureFlagEnvironmentScaffold(client *Client, projectKey, } // patch feature flag with env-specific config - patch := ldapi.PatchComment{ - Comment: "Terraform feature flag env data source test", - Patch: envConfigPatches, - } + patch := ldapi.NewPatchWithComment(envConfigPatches) + patch.SetComment("Terraform feature flag env data source test") + _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey, patch) + return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(*patch).Execute() }) }) if err != nil { @@ -54,7 +53,7 @@ func testAccDataSourceFeatureFlagEnvironmentScaffold(client *Client, projectKey, return nil, fmt.Errorf("failed to create feature flag env config: %s", err.Error()) } flagRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey, nil) + return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() }) if err != nil { _ = testAccDataSourceProjectDelete(client, projectKey) @@ -166,13 +165,13 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "env_key", envKey), resource.TestCheckResourceAttr(resourceName, "on", fmt.Sprint(thisConfig.On)), resource.TestCheckResourceAttr(resourceName, "track_events", fmt.Sprint(thisConfig.TrackEvents)), - resource.TestCheckResourceAttr(resourceName, "rules.0.variation", fmt.Sprint(thisConfig.Rules[0].Variation)), + resource.TestCheckResourceAttr(resourceName, "rules.0.variation", fmt.Sprint(*thisConfig.Rules[0].Variation)), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", thisConfig.Rules[0].Clauses[0].Attribute), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.op", thisConfig.Rules[0].Clauses[0].Op), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", fmt.Sprint(thisConfig.Rules[0].Clauses[0].Values[0])), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.flag_key", thisConfig.Prerequisites[0].Key), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.variation", fmt.Sprint(thisConfig.Prerequisites[0].Variation)), - resource.TestCheckResourceAttr(resourceName, "off_variation", fmt.Sprint(thisConfig.OffVariation)), + resource.TestCheckResourceAttr(resourceName, "off_variation", fmt.Sprint(*thisConfig.OffVariation)), resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", fmt.Sprint(len(thisConfig.Targets[0].Values))), resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "1"), ), diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_test.go index b95d6a4b..5371d023 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -29,7 +29,7 @@ func TestAccDataSourceFeatureFlag_noMatchReturnsError(t *testing.T) { client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) require.NoError(t, err) projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: "Terraform Flag Test Project", Key: projectKey, } @@ -71,13 +71,13 @@ func TestAccDataSourceFeatureFlag_exists(t *testing.T) { flagBody := ldapi.FeatureFlagBody{ Name: flagName, Key: flagKey, - Variations: []ldapi.Variation{ + Variations: &[]ldapi.Variation{ {Value: intfPtr(true)}, {Value: intfPtr(false)}, }, - Description: "a flag to test the terraform flag data source", - Temporary: true, - ClientSideAvailability: &ldapi.ClientSideAvailability{ + Description: ldapi.PtrString("a flag to test the terraform flag data source"), + Temporary: ldapi.PtrBool(true), + ClientSideAvailability: &ldapi.ClientSideAvailabilityPost{ UsingEnvironmentId: true, UsingMobileKey: false, }, @@ -105,7 +105,7 @@ func TestAccDataSourceFeatureFlag_exists(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "project_key"), resource.TestCheckResourceAttr(resourceName, "key", flag.Key), resource.TestCheckResourceAttr(resourceName, "name", flag.Name), - resource.TestCheckResourceAttr(resourceName, "description", flag.Description), + resource.TestCheckResourceAttr(resourceName, "description", *flag.Description), resource.TestCheckResourceAttr(resourceName, "temporary", "true"), resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), diff --git a/launchdarkly/data_source_launchdarkly_project_test.go b/launchdarkly/data_source_launchdarkly_project_test.go index 95a5a1c0..a83522cc 100644 --- a/launchdarkly/data_source_launchdarkly_project_test.go +++ b/launchdarkly/data_source_launchdarkly_project_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -57,25 +57,25 @@ func TestAccDataSourceProject_exists(t *testing.T) { client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) require.NoError(t, err) - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: projectName, Key: projectKey, - DefaultClientSideAvailability: &ldapi.ClientSideAvailability{ + DefaultClientSideAvailability: &ldapi.DefaultClientSideAvailabilityPost{ UsingEnvironmentId: false, UsingMobileKey: false, }, - Tags: []string{ + Tags: &[]string{ tag, }, - Environments: []ldapi.EnvironmentPost{ + Environments: &[]ldapi.EnvironmentPost{ { Name: envName, Key: envKey, Color: envColor, - SecureMode: true, - ConfirmChanges: true, - RequireComments: true, - Tags: []string{ + SecureMode: ldapi.PtrBool(true), + ConfirmChanges: ldapi.PtrBool(true), + RequireComments: ldapi.PtrBool(true), + Tags: &[]string{ tag, }, }, diff --git a/launchdarkly/data_source_launchdarkly_segment_test.go b/launchdarkly/data_source_launchdarkly_segment_test.go index 36d949cc..37b4db8c 100644 --- a/launchdarkly/data_source_launchdarkly_segment_test.go +++ b/launchdarkly/data_source_launchdarkly_segment_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -31,7 +31,7 @@ type testSegmentUpdate struct { func testAccDataSourceSegmentCreate(client *Client, projectKey, segmentKey string, properties testSegmentUpdate) (*ldapi.UserSegment, error) { envKey := "test" - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: "Terraform Segment DS Test", Key: projectKey, } @@ -40,27 +40,29 @@ func testAccDataSourceSegmentCreate(client *Client, projectKey, segmentKey strin return nil, err } - segmentBody := ldapi.UserSegmentBody{ + segmentBody := ldapi.SegmentBody{ Name: "Data Source Test Segment", Key: segmentKey, - Description: "test description", - Tags: []string{"terraform"}, + Description: ldapi.PtrString("test description"), + Tags: &[]string{"terraform"}, } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.PostUserSegment(client.ctx, project.Key, envKey, segmentBody) + return client.ld.SegmentsApi.PostSegment(client.ctx, project.Key, envKey).SegmentBody(segmentBody).Execute() }) if err != nil { return nil, fmt.Errorf("failed to create segment %q in project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) } - patch := []ldapi.PatchOperation{ - patchReplace("/included", properties.Included), - patchReplace("/excluded", properties.Excluded), - patchReplace("/rules", properties.Rules), + patch := ldapi.PatchWithComment{ + Patch: []ldapi.PatchOperation{ + patchReplace("/included", properties.Included), + patchReplace("/excluded", properties.Excluded), + patchReplace("/rules", properties.Rules), + }, } rawSegment, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.PatchUserSegment(client.ctx, projectKey, envKey, segmentKey, patch) + return client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, segmentKey).PatchWithComment(patch).Execute() }) }) if err != nil { @@ -83,7 +85,7 @@ func TestAccDataSourceSegment_noMatchReturnsError(t *testing.T) { segmentKey := "bad-segment-key" client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) require.NoError(t, err) - _, err = testAccDataSourceProjectCreate(client, ldapi.ProjectBody{Name: "Segment DS No Match Test", Key: projectKey}) + _, err = testAccDataSourceProjectCreate(client, ldapi.ProjectPost{Name: "Segment DS No Match Test", Key: projectKey}) require.NoError(t, err) defer func() { diff --git a/launchdarkly/data_source_launchdarkly_team_member.go b/launchdarkly/data_source_launchdarkly_team_member.go index a809e67d..821d88ba 100644 --- a/launchdarkly/data_source_launchdarkly_team_member.go +++ b/launchdarkly/data_source_launchdarkly_team_member.go @@ -4,9 +4,8 @@ import ( "fmt" "net/http" - "github.com/antihax/optional" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func dataSourceTeamMember() *schema.Resource { @@ -41,26 +40,25 @@ func dataSourceTeamMember() *schema.Resource { } func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, error) { - apiOpts := ldapi.TeamMembersApiGetMembersOpts{ - Limit: optional.NewFloat32(1000), // this should be the max limit allowed when the member-list-max-limit flag is on - } + // this should be the max limit allowed when the member-list-max-limit flag is on + teamMemberLimit := int64(1000) membersRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.TeamMembersApi.GetMembers(client.ctx, &apiOpts) + return client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Execute() }) if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) } members := membersRaw.(ldapi.Members) - totalMemberCount := int(members.TotalCount) + totalMemberCount := int(*members.TotalCount) memberItems := members.Items membersPulled := len(memberItems) for membersPulled < totalMemberCount { - apiOpts.Offset = optional.NewFloat32(float32(membersPulled)) + offset := int64(membersPulled) newRawMembers, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.TeamMembersApi.GetMembers(client.ctx, &apiOpts) + return client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Execute() }) if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) diff --git a/launchdarkly/data_source_launchdarkly_team_member_test.go b/launchdarkly/data_source_launchdarkly_team_member_test.go index 65d46172..0cd29d03 100644 --- a/launchdarkly/data_source_launchdarkly_team_member_test.go +++ b/launchdarkly/data_source_launchdarkly_team_member_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -21,14 +21,12 @@ data "launchdarkly_team_member" "test" { } func testAccDataSourceTeamMemberCreate(client *Client, email string) (*ldapi.Member, error) { - membersBody := ldapi.MembersBody{ + membersBody := []ldapi.NewMemberForm{{ Email: email, - FirstName: "Test", - LastName: "Account", - } - members, _, err := client.ld.TeamMembersApi.PostMembers(client.ctx, []ldapi.MembersBody{ - membersBody, - }) + FirstName: ldapi.PtrString("Test"), + LastName: ldapi.PtrString("Account"), + }} + members, _, err := client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm(membersBody).Execute() if err != nil { return nil, err } @@ -36,7 +34,7 @@ func testAccDataSourceTeamMemberCreate(client *Client, email string) (*ldapi.Mem } func testAccDataSourceTeamMemberDelete(client *Client, id string) error { - _, err := client.ld.TeamMembersApi.DeleteMember(client.ctx, id) + _, err := client.ld.AccountMembersApi.DeleteMember(client.ctx, id).Execute() if err != nil { return err } @@ -91,8 +89,8 @@ func TestAccDataSourceTeamMember_exists(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "email"), resource.TestCheckResourceAttr(resourceName, "email", testMember.Email), - resource.TestCheckResourceAttr(resourceName, "first_name", testMember.FirstName), - resource.TestCheckResourceAttr(resourceName, "last_name", testMember.LastName), + resource.TestCheckResourceAttr(resourceName, "first_name", *testMember.FirstName), + resource.TestCheckResourceAttr(resourceName, "last_name", *testMember.LastName), resource.TestCheckResourceAttr(resourceName, "id", testMember.Id), ), }, diff --git a/launchdarkly/data_source_launchdarkly_webhook_test.go b/launchdarkly/data_source_launchdarkly_webhook_test.go index 4fd66906..5d177c0f 100644 --- a/launchdarkly/data_source_launchdarkly_webhook_test.go +++ b/launchdarkly/data_source_launchdarkly_webhook_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -22,13 +22,13 @@ data "launchdarkly_webhook" "test" { ) func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi.Webhook, error) { - webhookBody := ldapi.WebhookBody{ + webhookBody := ldapi.WebhookPost{ Url: "https://www.example.com", Sign: false, On: true, - Name: webhookName, - Tags: []string{"terraform"}, - Statements: []ldapi.Statement{ + Name: ldapi.PtrString(webhookName), + Tags: &[]string{"terraform"}, + Statements: &[]ldapi.StatementPost{ { Resources: []string{"proj/*"}, Actions: []string{"turnFlagOn"}, @@ -37,7 +37,7 @@ func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi. }, } webhookRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PostWebhook(client.ctx, webhookBody) + return client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() }) if err != nil { return nil, fmt.Errorf("failed to create webhook with name %q: %s", webhookName, handleLdapiErr(err)) @@ -51,7 +51,7 @@ func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi. func testAccDataSourceWebhookDelete(client *Client, webhookId string) error { _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookId) + res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookId).Execute() return nil, res, err }) if err != nil { @@ -112,12 +112,13 @@ func TestAccDataSourceWebhook_exists(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", webhookName), resource.TestCheckResourceAttr(resourceName, "url", webhook.Url), resource.TestCheckResourceAttr(resourceName, "on", "true"), - resource.TestCheckResourceAttr(resourceName, "secret", webhook.Secret), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "turnFlagOn"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "secret", ""), // since we set Sign to false + ), }, }, diff --git a/launchdarkly/default_variations_helper.go b/launchdarkly/default_variations_helper.go index cbd15b68..5c58732c 100644 --- a/launchdarkly/default_variations_helper.go +++ b/launchdarkly/default_variations_helper.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func defaultVariationsFromResourceData(d *schema.ResourceData) (*ldapi.Defaults, error) { diff --git a/launchdarkly/default_variations_helper_test.go b/launchdarkly/default_variations_helper_test.go index 4ea95467..051e6685 100644 --- a/launchdarkly/default_variations_helper_test.go +++ b/launchdarkly/default_variations_helper_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index 4e022c99..a7ca3ba5 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // baseEnvironmentSchema covers the overlap between the data source and resource schemas @@ -175,7 +175,7 @@ func environmentPostFromResourceData(env interface{}) ldapi.EnvironmentPost { } if defaultTTL, ok := envMap[DEFAULT_TTL]; ok { - envPost.DefaultTtl = float32(defaultTTL.(int)) + envPost.DefaultTtl = ldapi.PtrInt32(int32(defaultTTL.(int))) } return envPost } @@ -225,7 +225,7 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool key := d.Get(KEY).(string) envRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projectKey, key) + return client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projectKey, key).Execute() }) if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find environment with key %q in project %q, removing from state", key, projectKey) diff --git a/launchdarkly/environments_helper_test.go b/launchdarkly/environments_helper_test.go index b2490e6f..b877d7b1 100644 --- a/launchdarkly/environments_helper_test.go +++ b/launchdarkly/environments_helper_test.go @@ -3,7 +3,7 @@ package launchdarkly import ( "testing" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/require" ) @@ -25,7 +25,7 @@ func TestEnvironmentPostFromResourceData(t *testing.T) { Name: "envName", Key: "envKey", Color: "000000", - DefaultTtl: 50, + DefaultTtl: ldapi.PtrInt32(50), }, }, { diff --git a/launchdarkly/fallthrough_helper.go b/launchdarkly/fallthrough_helper.go index 986f10dc..7b866a7c 100644 --- a/launchdarkly/fallthrough_helper.go +++ b/launchdarkly/fallthrough_helper.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func fallthroughSchema(forDataSource bool) *schema.Schema { @@ -75,7 +75,7 @@ func fallthroughFromResourceData(d *schema.ResourceData) (fallthroughModel, erro rollout := fallthroughModel{Rollout: rolloutFromResourceData(fall[ROLLOUT_WEIGHTS])} bucketBy, ok := fall[BUCKET_BY] if ok { - rollout.Rollout.BucketBy = bucketBy.(string) + rollout.Rollout.BucketBy = ldapi.PtrString(bucketBy.(string)) } return rollout, nil @@ -84,13 +84,13 @@ func fallthroughFromResourceData(d *schema.ResourceData) (fallthroughModel, erro return fallthroughModel{Variation: &val}, nil } -func fallthroughToResourceData(fallThrough *ldapi.ModelFallthrough) interface{} { +func fallthroughToResourceData(fallThrough ldapi.VariationOrRolloutRep) interface{} { transformed := make([]interface{}, 1) if fallThrough.Rollout != nil { rollout := map[string]interface{}{ ROLLOUT_WEIGHTS: rolloutsToResourceData(fallThrough.Rollout), } - if fallThrough.Rollout.BucketBy != "" { + if fallThrough.Rollout.BucketBy != nil { rollout[BUCKET_BY] = fallThrough.Rollout.BucketBy } transformed[0] = rollout diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index 8129da65..21dbd0ab 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -6,10 +6,9 @@ import ( "net/http" "strings" - "github.com/antihax/optional" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Schema { @@ -57,9 +56,7 @@ func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Sch // get FeatureFlagEnvironment uses a query parameter to get the ldapi.FeatureFlag with only a single environment. func getFeatureFlagEnvironment(client *Client, projectKey, flagKey, environmentKey string) (ldapi.FeatureFlag, *http.Response, error) { flagRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey, &ldapi.FeatureFlagsApiGetFeatureFlagOpts{ - Env: optional.NewInterface(environmentKey), - }) + return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Env(environmentKey).Execute() }) flag := flagRaw.(ldapi.FeatureFlag) return flag, res, err @@ -116,7 +113,7 @@ func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataS return fmt.Errorf("failed to set targets on flag with key %q: %v", flagKey, err) } - err = d.Set(FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough_)) + err = d.Set(FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough)) if err != nil { return fmt.Errorf("failed to set flag fallthrough on flag with key %q: %v", flagKey, err) } diff --git a/launchdarkly/feature_flags_helper.go b/launchdarkly/feature_flags_helper.go index 45f54449..2122d5b4 100644 --- a/launchdarkly/feature_flags_helper.go +++ b/launchdarkly/feature_flags_helper.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseFeatureFlagSchema() map[string]*schema.Schema { @@ -47,10 +47,33 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { Default: false, }, INCLUDE_IN_SNIPPET: { - Type: schema.TypeBool, - Optional: true, - Description: "Whether or not this flag should be made available to the client-side JavaScript SDK", - Default: false, + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Whether or not this flag should be made available to the client-side JavaScript SDK", + Deprecated: "'include_in_snippet' is now deprecated. Please migrate to 'client_side_availability' to maintain future compatability.", + ConflictsWith: []string{CLIENT_SIDE_AVAILABILITY}, + }, + // Annoying that we can't define a typemap to have specific keys https://www.terraform.io/docs/extend/schemas/schema-types.html#typemap + CLIENT_SIDE_AVAILABILITY: { + Type: schema.TypeList, + Optional: true, + Computed: true, + ConflictsWith: []string{INCLUDE_IN_SNIPPET}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + USING_ENVIRONMENT_ID: { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + USING_MOBILE_KEY: { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, }, TAGS: tagsSchema(), CUSTOM_PROPERTIES: customPropertiesSchema(), @@ -92,7 +115,7 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) key := d.Get(KEY).(string) flagRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key, nil) + return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key).Execute() }) flag := flagRaw.(ldapi.FeatureFlag) if isStatusNotFound(res) && !isDataSource { @@ -109,20 +132,17 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) _ = d.Set(KEY, flag.Key) _ = d.Set(NAME, flag.Name) _ = d.Set(DESCRIPTION, flag.Description) - _ = d.Set(INCLUDE_IN_SNIPPET, flag.IncludeInSnippet) _ = d.Set(TEMPORARY, flag.Temporary) _ = d.Set(ARCHIVED, flag.Archived) - if isDataSource { - CSA := *flag.ClientSideAvailability - clientSideAvailability := []map[string]interface{}{{ - "using_environment_id": CSA.UsingEnvironmentId, - "using_mobile_key": CSA.UsingMobileKey, - }} - _ = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) - } else { - _ = d.Set(INCLUDE_IN_SNIPPET, flag.IncludeInSnippet) - } + CSA := *flag.ClientSideAvailability + clientSideAvailability := []map[string]interface{}{{ + USING_ENVIRONMENT_ID: CSA.UsingEnvironmentId, + USING_MOBILE_KEY: CSA.UsingMobileKey, + }} + // Always set both CSA and IIS to state in order to correctly represent the flag resource as it exists in LD + _ = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) + _ = d.Set(INCLUDE_IN_SNIPPET, CSA.UsingEnvironmentId) // Only set the maintainer ID if is specified in the schema _, ok := d.GetOk(MAINTAINER_ID) @@ -184,3 +204,14 @@ func flagIdToKeys(id string) (projectKey string, flagKey string, err error) { projectKey, flagKey = parts[0], parts[1] return projectKey, flagKey, nil } + +func getProjectDefaultCSAandIncludeInSnippet(client *Client, projectKey string) (ldapi.ClientSideAvailability, bool, error) { + rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { + return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() + }) + if err != nil { + return ldapi.ClientSideAvailability{}, false, err + } + project := rawProject.(ldapi.Project) + return *project.DefaultClientSideAvailability, project.IncludeInSnippetByDefault, nil +} diff --git a/launchdarkly/helper.go b/launchdarkly/helper.go index cdce1cd1..390583f5 100644 --- a/launchdarkly/helper.go +++ b/launchdarkly/helper.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) const ( @@ -80,6 +80,8 @@ func intPtr(i int) *int { func strPtr(v string) *string { return &v } +func strArrayPtr(v []string) *[]string { return &v } + func patchReplace(path string, value interface{}) ldapi.PatchOperation { return ldapi.PatchOperation{ Op: "replace", @@ -109,7 +111,7 @@ func handleLdapiErr(err error) error { if err == nil { return nil } - if swaggerErr, ok := err.(ldapi.GenericSwaggerError); ok { + if swaggerErr, ok := err.(ldapi.GenericOpenAPIError); ok { return fmt.Errorf("%s: %s", swaggerErr.Error(), string(swaggerErr.Body())) } return err diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 12890b49..f7ebc1bc 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -84,4 +84,6 @@ const ( MIN_NUM_APPROVALS = "min_num_approvals" CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" REQUIRED_APPROVAL_TAGS = "required_approval_tags" + USING_ENVIRONMENT_ID = "using_environment_id" + USING_MOBILE_KEY = "using_mobile_key" ) diff --git a/launchdarkly/policies_helper.go b/launchdarkly/policies_helper.go index 6cb1b606..5105427e 100644 --- a/launchdarkly/policies_helper.go +++ b/launchdarkly/policies_helper.go @@ -5,7 +5,7 @@ import ( "sort" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func policyArraySchema() *schema.Schema { @@ -39,10 +39,10 @@ func policyArraySchema() *schema.Schema { } } -func policiesFromResourceData(d *schema.ResourceData) []ldapi.Policy { +func policiesFromResourceData(d *schema.ResourceData) []ldapi.StatementPost { schemaPolicies := d.Get(POLICY).(*schema.Set) - policies := make([]ldapi.Policy, schemaPolicies.Len()) + policies := make([]ldapi.StatementPost, schemaPolicies.Len()) list := schemaPolicies.List() for i, policy := range list { v := policyFromResourceData(policy) @@ -51,9 +51,9 @@ func policiesFromResourceData(d *schema.ResourceData) []ldapi.Policy { return policies } -func policyFromResourceData(val interface{}) ldapi.Policy { +func policyFromResourceData(val interface{}) ldapi.StatementPost { policyMap := val.(map[string]interface{}) - p := ldapi.Policy{ + p := ldapi.StatementPost{ Resources: []string{}, Actions: []string{}, Effect: policyMap[EFFECT].(string), @@ -70,7 +70,7 @@ func policyFromResourceData(val interface{}) ldapi.Policy { return p } -func policiesToResourceData(policies []ldapi.Policy) interface{} { +func policiesToResourceData(policies []ldapi.Statement) interface{} { transformed := make([]interface{}, len(policies)) for i, p := range policies { diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index ad3df49e..36b6a50b 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // policyStatementSchemaOptions is used to help with renaming 'policy_statements' to statements for the launchdarkly_webhook resource. @@ -93,8 +93,8 @@ func validatePolicyStatement(statement map[string]interface{}) error { return nil } -func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.Statement, error) { - statements := make([]ldapi.Statement, 0, len(schemaStatements)) +func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.StatementPost, error) { + statements := make([]ldapi.StatementPost, 0, len(schemaStatements)) for _, stmt := range schemaStatements { statement := stmt.(map[string]interface{}) err := validatePolicyStatement(statement) @@ -107,60 +107,74 @@ func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.S return statements, nil } -func policyStatementFromResourceData(statement map[string]interface{}) ldapi.Statement { - ret := ldapi.Statement{ +func policyStatementFromResourceData(statement map[string]interface{}) ldapi.StatementPost { + ret := ldapi.StatementPost{ Effect: statement[EFFECT].(string), } for _, r := range statement[RESOURCES].([]interface{}) { ret.Resources = append(ret.Resources, r.(string)) } - for _, n := range statement[NOT_RESOURCES].([]interface{}) { - ret.NotResources = append(ret.NotResources, n.(string)) - } for _, a := range statement[ACTIONS].([]interface{}) { ret.Actions = append(ret.Actions, a.(string)) } - for _, n := range statement[NOT_ACTIONS].([]interface{}) { - ret.NotActions = append(ret.NotActions, n.(string)) + // optional fields + rawNotResources := statement[NOT_RESOURCES].([]interface{}) + var notResources []string + for _, n := range rawNotResources { + notResources = append(notResources, n.(string)) + ret.NotResources = ¬Resources + } + rawNotActions := statement[NOT_ACTIONS].([]interface{}) + var notActions []string + for _, n := range rawNotActions { + notActions = append(notActions, n.(string)) + ret.NotActions = ¬Actions } return ret } -func policyStatementsToResourceData(statements []ldapi.Statement) []interface{} { +func policyStatementsToResourceData(statements []ldapi.StatementRep) []interface{} { transformed := make([]interface{}, 0, len(statements)) for _, s := range statements { t := map[string]interface{}{ EFFECT: s.Effect, } - if len(s.Resources) > 0 { - t[RESOURCES] = stringSliceToInterfaceSlice(s.Resources) + if s.Resources != nil && len(*s.Resources) > 0 { + var resources []interface{} + for _, v := range *s.Resources { + resources = append(resources, v) + } + t[RESOURCES] = resources } - if len(s.NotResources) > 0 { - t[NOT_RESOURCES] = stringSliceToInterfaceSlice(s.NotResources) + if s.NotResources != nil && len(*s.NotResources) > 0 { + var notResources []interface{} + for _, v := range *s.NotResources { + notResources = append(notResources, v) + } + t[NOT_RESOURCES] = notResources } - if len(s.Actions) > 0 { - t[ACTIONS] = stringSliceToInterfaceSlice(s.Actions) + if s.Actions != nil && len(*s.Actions) > 0 { + t[ACTIONS] = stringSliceToInterfaceSlice(*s.Actions) } - if len(s.NotActions) > 0 { - t[NOT_ACTIONS] = stringSliceToInterfaceSlice(s.NotActions) + if s.NotActions != nil && len(*s.NotActions) > 0 { + t[NOT_ACTIONS] = stringSliceToInterfaceSlice(*s.NotActions) } transformed = append(transformed, t) } return transformed } -func statementsToPolicies(statements []ldapi.Statement) []ldapi.Policy { - policies := make([]ldapi.Policy, 0, len(statements)) - for _, s := range statements { - policies = append(policies, ldapi.Policy(s)) - } - return policies -} - -func policiesToStatements(policies []ldapi.Policy) []ldapi.Statement { - statements := make([]ldapi.Statement, 0, len(policies)) +func statementsToStatementReps(policies []ldapi.Statement) []ldapi.StatementRep { + statements := make([]ldapi.StatementRep, 0, len(policies)) for _, p := range policies { - statements = append(statements, ldapi.Statement(p)) + rep := ldapi.StatementRep{ + Resources: p.Resources, + Actions: p.Actions, + NotResources: p.NotResources, + NotActions: p.NotActions, + Effect: p.Effect, + } + statements = append(statements, rep) } return statements } diff --git a/launchdarkly/policy_statements_helper_test.go b/launchdarkly/policy_statements_helper_test.go index a8404606..2efddfe1 100644 --- a/launchdarkly/policy_statements_helper_test.go +++ b/launchdarkly/policy_statements_helper_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,7 +13,7 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { testCases := []struct { name string policyStatements map[string]interface{} - expected []ldapi.Statement + expected []ldapi.StatementPost }{ { name: "basic policy statement", @@ -26,7 +26,7 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, }, }, - expected: []ldapi.Statement{ + expected: []ldapi.StatementPost{ { Resources: []string{"proj/*"}, Actions: []string{"*"}, @@ -50,7 +50,7 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, }, }, - expected: []ldapi.Statement{ + expected: []ldapi.StatementPost{ { Resources: []string{"proj/*:env/*;qa_*"}, Actions: []string{"*"}, @@ -74,9 +74,9 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, }, }, - expected: []ldapi.Statement{ + expected: []ldapi.StatementPost{ { - NotResources: []string{"proj/*:env/production:flag/*"}, + NotResources: strArrayPtr([]string{"proj/*:env/production:flag/*"}), Actions: []string{"*"}, Effect: "allow", }, @@ -97,7 +97,9 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { require.NoError(t, err) require.Equal(t, tc.expected, actual) - actualRaw := policyStatementsToResourceData(actual) + // with v7 of the go client there is an accidental duplicate type, so it returns a Statement type + // even though it takes a StatementPost type + actualRaw := policyStatementsToResourceData(statementsToStatementReps(statementPostsToStatements(actual))) require.Equal(t, tc.policyStatements[POLICY_STATEMENTS], actualRaw) }) } @@ -161,3 +163,21 @@ func TestPolicyStatementValidation(t *testing.T) { }) } } + +// statementPostToStatement is a helper function just for these tests +// since v7 of the go client passes and returns two differing types +func statementPostsToStatements(posts []ldapi.StatementPost) []ldapi.Statement { + var statements []ldapi.Statement + for _, p := range posts { + p := p + statement := ldapi.Statement{ + Resources: &p.Resources, + NotResources: p.NotResources, + Actions: &p.Actions, + NotActions: p.NotActions, + Effect: p.Effect, + } + statements = append(statements, statement) + } + return statements +} diff --git a/launchdarkly/prerequisite_helper.go b/launchdarkly/prerequisite_helper.go index 812f4c0c..682da0a0 100644 --- a/launchdarkly/prerequisite_helper.go +++ b/launchdarkly/prerequisite_helper.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func prerequisitesSchema() *schema.Schema { diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index 566305e2..78779578 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) error { @@ -14,7 +14,7 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er projectKey := d.Get(KEY).(string) rawProject, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey) + return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() }) // return nil error for resource reads but 404 for data source reads if isStatusNotFound(res) && !isDataSource { diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 4041863b..9cf1b6d7 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -2,6 +2,8 @@ package launchdarkly import ( "fmt" + "net/url" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -70,11 +72,15 @@ func Provider() *schema.Provider { func providerConfigure(d *schema.ResourceData) (interface{}, error) { host := d.Get(api_host).(string) + if strings.HasPrefix(host, "http") { + u, _ := url.Parse(host) + host = u.Host + } accessToken := d.Get(access_token).(string) oauthToken := d.Get(oauth_token).(string) if oauthToken == "" && accessToken == "" { - return nil, fmt.Errorf("either an %q or %q must be specified.", access_token, oauth_token) + return nil, fmt.Errorf("either an %q or %q must be specified", access_token, oauth_token) } if oauthToken != "" { diff --git a/launchdarkly/resource_launchdarkly_access_token.go b/launchdarkly/resource_launchdarkly_access_token.go index 156bbfbc..34df7b7e 100644 --- a/launchdarkly/resource_launchdarkly_access_token.go +++ b/launchdarkly/resource_launchdarkly_access_token.go @@ -1,15 +1,18 @@ package launchdarkly import ( + "bytes" + "encoding/json" "fmt" + "io" "log" "net/http" + "strings" - "github.com/antihax/optional" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceAccessToken() *schema.Resource { @@ -49,7 +52,7 @@ func resourceAccessToken() *schema.Resource { Set: schema.HashString, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, - ConflictsWith: []string{ROLE, POLICY_STATEMENTS}, + ConflictsWith: []string{ROLE, POLICY_STATEMENTS, INLINE_ROLES}, }, POLICY_STATEMENTS: deprecatedTokenPolicySchema, INLINE_ROLES: tokenPolicySchema, @@ -75,10 +78,11 @@ func resourceAccessToken() *schema.Resource { Sensitive: true, }, EXPIRE: { - Deprecated: "'expire' is deprecated and will be removed in the next major release of the LaunchDarly provider", - Type: schema.TypeInt, - Description: "Replace the computed token secret with a new value. The expired secret will no longer be able to authorize usage of the LaunchDarkly API. Should be an expiration time for the current token secret, expressed as a Unix epoch time in milliseconds. Setting this to a negative value will expire the existing token immediately. To reset the token value again, change 'expire' to a new value. Setting this field at resource creation time WILL NOT set an expiration time for the token.", - Optional: true, + Deprecated: "'expire' is deprecated and will be removed in the next major release of the LaunchDarkly provider", + Type: schema.TypeInt, + Description: "Replace the computed token secret with a new value. The expired secret will no longer be able to authorize usage of the LaunchDarkly API. Should be an expiration time for the current token secret, expressed as a Unix epoch time in milliseconds. Setting this to a negative value will expire the existing token immediately. To reset the token value again, change 'expire' to a new value. Setting this field at resource creation time WILL NOT set an expiration time for the token.", + Optional: true, + ValidateFunc: validation.NoZeroValues, }, }, } @@ -123,31 +127,37 @@ func resourceAccessTokenCreate(d *schema.ResourceData, metaRaw interface{}) erro client := metaRaw.(*Client) accessTokenName := d.Get(NAME).(string) - accessTokenRole := d.Get(ROLE).(string) serviceToken := d.Get(SERVICE_TOKEN).(bool) - defaultApiVersion := d.Get(DEFAULT_API_VERSION).(int) - customRolesRaw := d.Get(CUSTOM_ROLES).(*schema.Set).List() + + accessTokenBody := ldapi.AccessTokenPost{ + Name: ldapi.PtrString(accessTokenName), + ServiceToken: ldapi.PtrBool(serviceToken), + } + + if defaultApiVersion, ok := d.GetOk(DEFAULT_API_VERSION); ok { + accessTokenBody.DefaultApiVersion = ldapi.PtrInt32(int32(defaultApiVersion.(int))) + } + inlineRoles, _ := policyStatementsFromResourceData(d.Get(POLICY_STATEMENTS).([]interface{})) if len(inlineRoles) == 0 { inlineRoles, _ = policyStatementsFromResourceData(d.Get(INLINE_ROLES).([]interface{})) } - customRoles := make([]string, len(customRolesRaw)) - for i, cr := range customRolesRaw { - customRoles[i] = cr.(string) - } - - accessTokenBody := ldapi.TokenBody{ - Name: accessTokenName, - Role: accessTokenRole, - CustomRoleIds: customRoles, - InlineRole: inlineRoles, - ServiceToken: serviceToken, - DefaultApiVersion: int32(defaultApiVersion), + customRolesRaw := d.Get(CUSTOM_ROLES).(*schema.Set).List() + if len(inlineRoles) == 0 && len(customRolesRaw) > 0 { + customRoles := make([]string, len(customRolesRaw)) + for i, cr := range customRolesRaw { + customRoles[i] = cr.(string) + } + accessTokenBody.CustomRoleIds = &customRoles + } else if len(inlineRoles) > 0 { + accessTokenBody.InlineRole = &inlineRoles + } else if accessTokenRole, ok := d.GetOk(ROLE); ok { + accessTokenBody.Role = ldapi.PtrString(accessTokenRole.(string)) } tokenRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.PostToken(client.ctx, accessTokenBody) + return client.ld.AccessTokensApi.PostToken(client.ctx).AccessTokenPost(accessTokenBody).Execute() }) token := tokenRaw.(ldapi.Token) if err != nil { @@ -164,7 +174,7 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error accessTokenID := d.Id() accessTokenRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.GetToken(client.ctx, accessTokenID) + return client.ld.AccessTokensApi.GetToken(client.ctx, accessTokenID).Execute() }) accessToken := accessTokenRaw.(ldapi.Token) if isStatusNotFound(res) { @@ -177,11 +187,11 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error } _ = d.Set(NAME, accessToken.Name) - if accessToken.Role != "" { - _ = d.Set(ROLE, accessToken.Role) + if accessToken.Role != nil { + _ = d.Set(ROLE, *accessToken.Role) } - if len(accessToken.CustomRoleIds) > 0 { - customRoleKeys, err := customRoleIDsToKeys(client, accessToken.CustomRoleIds) + if accessToken.CustomRoleIds != nil && len(*accessToken.CustomRoleIds) > 0 { + customRoleKeys, err := customRoleIDsToKeys(client, *accessToken.CustomRoleIds) if err != nil { return err } @@ -191,12 +201,12 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error _ = d.Set(DEFAULT_API_VERSION, accessToken.DefaultApiVersion) policies := accessToken.InlineRole - if len(policies) > 0 { + if policies != nil && len(*policies) > 0 { policyStatements, _ := policyStatementsFromResourceData(d.Get(POLICY_STATEMENTS).([]interface{})) if len(policyStatements) > 0 { - err = d.Set(POLICY_STATEMENTS, policyStatementsToResourceData(policies)) + err = d.Set(POLICY_STATEMENTS, policyStatementsToResourceData(*policies)) } else { - err = d.Set(INLINE_ROLES, policyStatementsToResourceData(policies)) + err = d.Set(INLINE_ROLES, policyStatementsToResourceData(*policies)) } if err != nil { return fmt.Errorf("could not set policy on access token with id %q: %v", accessTokenID, err) @@ -231,7 +241,7 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro if len(inlineRoles) == 0 { inlineRoles, _ = policyStatementsFromResourceData(d.Get(INLINE_ROLES).([]interface{})) } - iRoles := statementsToPolicies(inlineRoles) + iRoles := inlineRoles patch := []ldapi.PatchOperation{ patchReplace("/name", &accessTokenName), @@ -266,7 +276,7 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.PatchToken(client.ctx, accessTokenID, patch) + return client.ld.AccessTokensApi.PatchToken(client.ctx, accessTokenID).PatchOperation(patch).Execute() }) if err != nil { return fmt.Errorf("failed to update access token with id %q: %s", accessTokenID, handleLdapiErr(err)) @@ -277,12 +287,8 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro oldExpireRaw, newExpireRaw := d.GetChange(EXPIRE) oldExpire := oldExpireRaw.(int) newExpire := newExpireRaw.(int) - opts := ldapi.AccessTokensApiResetTokenOpts{} if oldExpire != newExpire && newExpire != 0 { - if newExpire > 0 { - opts.Expiry = optional.NewInt64(int64(newExpire)) - } - token, _, err := client.ld.AccessTokensApi.ResetToken(client.ctx, accessTokenID, &opts) + token, err := resetAccessToken(client, accessTokenID, newExpire) if err != nil { return fmt.Errorf("failed to reset access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } @@ -299,7 +305,7 @@ func resourceAccessTokenDelete(d *schema.ResourceData, metaRaw interface{}) erro accessTokenID := d.Id() _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.AccessTokensApi.DeleteToken(client.ctx, accessTokenID) + res, err := client.ld.AccessTokensApi.DeleteToken(client.ctx, accessTokenID).Execute() return nil, res, err }) if err != nil { @@ -314,7 +320,7 @@ func resourceAccessTokenExists(d *schema.ResourceData, metaRaw interface{}) (boo } func accessTokenExists(accessTokenID string, meta *Client) (bool, error) { - _, res, err := meta.ld.AccessTokensApi.GetToken(meta.ctx, accessTokenID) + _, res, err := meta.ld.AccessTokensApi.GetToken(meta.ctx, accessTokenID).Execute() if isStatusNotFound(res) { return false, nil } @@ -324,3 +330,55 @@ func accessTokenExists(accessTokenID string, meta *Client) (bool, error) { return true, nil } + +func resetAccessToken(client *Client, accessTokenID string, expiry int) (ldapi.Token, error) { + var token ldapi.Token + // var err error + // // Terraform validation will ensure we do not get a zero value + // if expiry > 0 { + // token, _, err = client.ld.AccessTokensApi.ResetToken(client.ctx, accessTokenID).Expiry(int64(expiry)).Execute() + // } else if expiry < 0 { + // token, _, err = client.ld.AccessTokensApi.ResetToken(client.ctx, accessTokenID).Execute() + // } + // if err != nil { + // return token, fmt.Errorf("failed to reset access token with id %q: %s", accessTokenID, handleLdapiErr(err)) + // } + // return token, nil + endpoint := fmt.Sprintf("%s/api/v2/tokens/%s/reset", client.apiHost, accessTokenID) + if !strings.HasPrefix(endpoint, "http") { + endpoint = "https://" + endpoint + } + var body io.Reader + if expiry > 0 { + rawBody, err := json.Marshal(map[string]int{ + "expiry": expiry, + }) + if err != nil { + return token, err + } + body = bytes.NewBuffer(rawBody) + } + req, err := http.NewRequest("POST", endpoint, body) + if err != nil { + return token, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", client.apiKey) + + resp, err := client.fallbackClient.Do(req) + if err != nil { + return token, err + } + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return token, err + } + + err = json.Unmarshal(rawBody, &token) + if err != nil { + return token, err + } + + return token, nil +} diff --git a/launchdarkly/resource_launchdarkly_access_token_test.go b/launchdarkly/resource_launchdarkly_access_token_test.go index 76744ce7..cf14544d 100644 --- a/launchdarkly/resource_launchdarkly_access_token_test.go +++ b/launchdarkly/resource_launchdarkly_access_token_test.go @@ -386,7 +386,7 @@ func testAccCheckAccessTokenExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("access token ID is not set") } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.AccessTokensApi.GetToken(client.ctx, rs.Primary.ID) + _, _, err := client.ld.AccessTokensApi.GetToken(client.ctx, rs.Primary.ID).Execute() if err != nil { return fmt.Errorf("received an error getting access token. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_custom_role.go b/launchdarkly/resource_launchdarkly_custom_role.go index 4b32f21b..6447372b 100644 --- a/launchdarkly/resource_launchdarkly_custom_role.go +++ b/launchdarkly/resource_launchdarkly_custom_role.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceCustomRole() *schema.Resource { @@ -57,18 +57,18 @@ func resourceCustomRoleCreate(d *schema.ResourceData, metaRaw interface{}) error return err } if len(policyStatements) > 0 { - customRolePolicies = statementsToPolicies(policyStatements) + customRolePolicies = policyStatements } - customRoleBody := ldapi.CustomRoleBody{ + customRoleBody := ldapi.CustomRolePost{ Key: customRoleKey, Name: customRoleName, - Description: customRoleDescription, + Description: ldapi.PtrString(customRoleDescription), Policy: customRolePolicies, } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.PostCustomRole(client.ctx, customRoleBody) + return client.ld.CustomRolesApi.PostCustomRole(client.ctx).CustomRolePost(customRoleBody).Execute() }) if err != nil { return fmt.Errorf("failed to create custom role with name %q: %s", customRoleName, handleLdapiErr(err)) @@ -83,7 +83,7 @@ func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { customRoleID := d.Id() customRoleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID) + return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() }) customRole := customRoleRaw.(ldapi.CustomRole) if isStatusNotFound(res) { @@ -104,7 +104,7 @@ func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { if _, ok := d.GetOk(POLICY); ok { err = d.Set(POLICY, policiesToResourceData(customRole.Policy)) } else { - err = d.Set(POLICY_STATEMENTS, policyStatementsToResourceData(policiesToStatements(customRole.Policy))) + err = d.Set(POLICY_STATEMENTS, policyStatementsToResourceData(statementsToStatementReps(customRole.Policy))) } if err != nil { @@ -124,18 +124,19 @@ func resourceCustomRoleUpdate(d *schema.ResourceData, metaRaw interface{}) error return err } if len(policyStatements) > 0 { - customRolePolicies = statementsToPolicies(policyStatements) + customRolePolicies = policyStatements } - patch := []ldapi.PatchOperation{ - patchReplace("/name", &customRoleName), - patchReplace("/description", &customRoleDescription), - patchReplace("/policy", &customRolePolicies), - } + patch := ldapi.PatchWithComment{ + Patch: []ldapi.PatchOperation{ + patchReplace("/name", &customRoleName), + patchReplace("/description", &customRoleDescription), + patchReplace("/policy", &customRolePolicies), + }} _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.PatchCustomRole(client.ctx, customRoleKey, patch) + return client.ld.CustomRolesApi.PatchCustomRole(client.ctx, customRoleKey).PatchWithComment(patch).Execute() }) }) if err != nil { @@ -150,7 +151,7 @@ func resourceCustomRoleDelete(d *schema.ResourceData, metaRaw interface{}) error customRoleKey := d.Id() _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.CustomRolesApi.DeleteCustomRole(client.ctx, customRoleKey) + res, err := client.ld.CustomRolesApi.DeleteCustomRole(client.ctx, customRoleKey).Execute() return nil, res, err }) @@ -166,7 +167,7 @@ func resourceCustomRoleExists(d *schema.ResourceData, metaRaw interface{}) (bool } func customRoleExists(customRoleKey string, meta *Client) (bool, error) { - _, res, err := meta.ld.CustomRolesApi.GetCustomRole(meta.ctx, customRoleKey) + _, res, err := meta.ld.CustomRolesApi.GetCustomRole(meta.ctx, customRoleKey).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_custom_role_test.go b/launchdarkly/resource_launchdarkly_custom_role_test.go index f5b073d7..06e1ec94 100644 --- a/launchdarkly/resource_launchdarkly_custom_role_test.go +++ b/launchdarkly/resource_launchdarkly_custom_role_test.go @@ -37,7 +37,7 @@ resource "launchdarkly_custom_role" "test" { resource "launchdarkly_custom_role" "test" { key = "%s" name = "Custom role - %s" - description= "Allow all actions on staging environments" + description = "Allow all actions on staging environments" policy_statements { actions = ["*"] effect = "allow" @@ -204,7 +204,7 @@ func testAccCheckCustomRoleExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("custom role ID is not set") } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.CustomRolesApi.GetCustomRole(client.ctx, rs.Primary.ID) + _, _, err := client.ld.CustomRolesApi.GetCustomRole(client.ctx, rs.Primary.ID).Execute() if err != nil { return fmt.Errorf("received an error getting custom role. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_destination.go b/launchdarkly/resource_launchdarkly_destination.go index f904aa2d..2a8561eb 100644 --- a/launchdarkly/resource_launchdarkly_destination.go +++ b/launchdarkly/resource_launchdarkly_destination.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceDestination() *schema.Resource { @@ -79,15 +79,15 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro return err } - destinationBody := ldapi.DestinationBody{ - Name: destinationName, - Kind: destinationKind, + destinationBody := ldapi.DestinationPost{ + Name: &destinationName, + Kind: &destinationKind, Config: &destinationConfig, - On: destinationOn, + On: &destinationOn, } destinationRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.PostDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationBody) + return client.ld.DataExportDestinationsApi.PostDestination(client.ctx, destinationProjKey, destinationEnvKey).DestinationPost(destinationBody).Execute() }) destination := destinationRaw.(ldapi.Destination) if err != nil { @@ -96,7 +96,7 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro } // destination defined in api-client-go/model_destination.go - d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, destination.Id}, "/")) + d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, *destination.Id}, "/")) return resourceDestinationRead(d, metaRaw) } @@ -112,7 +112,7 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error destinationEnvKey := d.Get(ENV_KEY).(string) destinationRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID) + return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() }) destination := destinationRaw.(ldapi.Destination) if isStatusNotFound(res) { @@ -124,7 +124,7 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error return fmt.Errorf("failed to get destination with id %q: %s", destinationID, handleLdapiErr(err)) } - cfg := destinationConfigToResourceData(destination.Kind, *destination.Config) + cfg := destinationConfigToResourceData(*destination.Kind, destination.Config) preservedCfg := preserveObfuscatedConfigAttributes(d.Get(CONFIG).(map[string]interface{}), cfg) _ = d.Set(NAME, destination.Name) @@ -132,7 +132,7 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error _ = d.Set(CONFIG, preservedCfg) _ = d.Set(ON, destination.On) - d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, destination.Id}, "/")) + d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, *destination.Id}, "/")) return nil } @@ -161,7 +161,7 @@ func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict((func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.PatchDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID, patch) + return client.ld.DataExportDestinationsApi.PatchDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).PatchOperation(patch).Execute() })) }) if err != nil { @@ -181,7 +181,7 @@ func resourceDestinationDelete(d *schema.ResourceData, metaRaw interface{}) erro destinationEnvKey := d.Get(ENV_KEY).(string) _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.DataExportDestinationsApi.DeleteDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID) + res, err := client.ld.DataExportDestinationsApi.DeleteDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() return nil, res, err }) @@ -202,7 +202,7 @@ func resourceDestinationExists(d *schema.ResourceData, metaRaw interface{}) (boo destinationEnvKey := d.Get(ENV_KEY).(string) _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID) + return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() }) if isStatusNotFound(res) { return false, nil diff --git a/launchdarkly/resource_launchdarkly_destination_test.go b/launchdarkly/resource_launchdarkly_destination_test.go index 97309f42..0a9a45c3 100644 --- a/launchdarkly/resource_launchdarkly_destination_test.go +++ b/launchdarkly/resource_launchdarkly_destination_test.go @@ -521,7 +521,7 @@ func testAccCheckDestinationExists(resourceName string) resource.TestCheckFunc { if err != nil { return err } - _, _, err = client.ld.DataExportDestinationsApi.GetDestination(client.ctx, projKey, envKey, destID) + _, _, err = client.ld.DataExportDestinationsApi.GetDestination(client.ctx, projKey, envKey, destID).Execute() if err != nil { return fmt.Errorf("error getting destination: %s", err) } diff --git a/launchdarkly/resource_launchdarkly_environment.go b/launchdarkly/resource_launchdarkly_environment.go index c85125dc..31665b33 100644 --- a/launchdarkly/resource_launchdarkly_environment.go +++ b/launchdarkly/resource_launchdarkly_environment.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceEnvironment() *schema.Resource { @@ -39,7 +39,7 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro key := d.Get(KEY).(string) name := d.Get(NAME).(string) color := d.Get(COLOR).(string) - defaultTTL := float32(d.Get(DEFAULT_TTL).(int)) + defaultTTL := int32(d.Get(DEFAULT_TTL).(int)) secureMode := d.Get(SECURE_MODE).(bool) defaultTrackEvents := d.Get(DEFAULT_TRACK_EVENTS).(bool) tags := stringsFromSchemaSet(d.Get(TAGS).(*schema.Set)) @@ -50,16 +50,16 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro Name: name, Key: key, Color: color, - DefaultTtl: defaultTTL, - SecureMode: secureMode, - DefaultTrackEvents: defaultTrackEvents, - Tags: tags, - RequireComments: requireComments, - ConfirmChanges: confirmChanges, + DefaultTtl: &defaultTTL, + SecureMode: &secureMode, + DefaultTrackEvents: &defaultTrackEvents, + Tags: &tags, + RequireComments: &requireComments, + ConfirmChanges: &confirmChanges, } _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey, envPost) + return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() }) if err != nil { return fmt.Errorf("failed to create environment: [%+v] for project key: %s: %s", envPost, projectKey, handleLdapiErr(err)) @@ -70,7 +70,7 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro err = resourceEnvironmentUpdate(d, metaRaw) if err != nil { // if there was a problem in the update state, we need to clean up completely by deleting the env - _, deleteErr := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key) + _, deleteErr := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() if deleteErr != nil { return fmt.Errorf("failed to clean up environment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -118,7 +118,7 @@ func resourceEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) erro patch = append(patch, approvalPatch...) _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key, patch) + return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key).PatchOperation(patch).Execute() }) }) if err != nil { @@ -134,7 +134,7 @@ func resourceEnvironmentDelete(d *schema.ResourceData, metaRaw interface{}) erro key := d.Get(KEY).(string) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key) + res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() return nil, res, err }) @@ -151,7 +151,7 @@ func resourceEnvironmentExists(d *schema.ResourceData, metaRaw interface{}) (boo func environmentExists(projectKey string, key string, meta *Client) (bool, error) { _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.EnvironmentsApi.GetEnvironment(meta.ctx, projectKey, key) + return meta.ld.EnvironmentsApi.GetEnvironment(meta.ctx, projectKey, key).Execute() }) if isStatusNotFound(res) { return false, nil diff --git a/launchdarkly/resource_launchdarkly_environment_test.go b/launchdarkly/resource_launchdarkly_environment_test.go index 20afd84c..e269f4f6 100644 --- a/launchdarkly/resource_launchdarkly_environment_test.go +++ b/launchdarkly/resource_launchdarkly_environment_test.go @@ -338,7 +338,7 @@ func testAccCheckEnvironmentExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("project key not found: %s", resourceName) } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projKey, envKey) + _, _, err := client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projKey, envKey).Execute() if err != nil { return fmt.Errorf("received an error getting environment. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 44480383..2420b5fe 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -1,13 +1,56 @@ package launchdarkly import ( + "context" "fmt" "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) +// We assign a custom diff in cases where the customer has not assigned CSA or IIS in config for a flag in order to respect project level defaults +func customizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + config := diff.GetRawConfig() + client := v.(*Client) + projectKey := diff.Get(PROJECT_KEY).(string) + + // Below values will exist due to the schema, we need to check if they are all null + snippetInConfig := config.GetAttr(INCLUDE_IN_SNIPPET) + csaInConfig := config.GetAttr(CLIENT_SIDE_AVAILABILITY) + + // If we have no keys in the CSA block in the config (length is 0) we know the customer hasn't set any CSA values + csaKeys := csaInConfig.AsValueSlice() + if len(csaKeys) == 0 { + // When we have no values for either clienSideAvailability or includeInSnippet + // Force an UPDATE call by setting a new value for INCLUDE_IN_SNIPPET in the diff according to project defaults + if snippetInConfig.IsNull() { + defaultCSA, includeInSnippetByDefault, err := getProjectDefaultCSAandIncludeInSnippet(client, projectKey) + // We will fall into this block during the first config read when a user creates a flag at the same time they create the parent project + // (and during our tests) + // We can ignore the error here, as it is correctly handled during update/create (and doesn't occur then as the project will have been created) + if err != nil { + } else { + // We set our values to the project defaults in order to guarantee an update call happening + // If we don't do this, we can run into an edge case described below + // IF previous value of INCLUDE_IN_SNIPPET was false + // AND the project default value for INCLUDE_IN_SNIPPET is true + // AND the customer removes the INCLUDE_IN_SNIPPET key from the config without replacing with defaultCSA + // The read would assume no changes are needed, HOWEVER we need to jump back to project level set defaults + // Hence the setting below + diff.SetNew(INCLUDE_IN_SNIPPET, includeInSnippetByDefault) + diff.SetNew(CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ + USING_ENVIRONMENT_ID: defaultCSA.UsingEnvironmentId, + USING_MOBILE_KEY: defaultCSA.UsingMobileKey, + }}) + } + } + + } + + return nil +} + func resourceFeatureFlag() *schema.Resource { schemaMap := baseFeatureFlagSchema() schemaMap[NAME] = &schema.Schema{ @@ -26,7 +69,8 @@ func resourceFeatureFlag() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceFeatureFlagImport, }, - Schema: schemaMap, + Schema: schemaMap, + CustomizeDiff: customizeDiff, } } @@ -46,6 +90,14 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro flagName := d.Get(NAME).(string) tags := stringsFromResourceData(d, TAGS) includeInSnippet := d.Get(INCLUDE_IN_SNIPPET).(bool) + // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk + // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) + _, clientSideAvailabilityOk := d.GetOk(CLIENT_SIDE_AVAILABILITY) + clientSideAvailability := &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: d.Get("client_side_availability.0.using_environment_id").(bool), + UsingMobileKey: d.Get("client_side_availability.0.using_mobile_key").(bool), + } temporary := d.Get(TEMPORARY).(bool) variations, err := variationsFromResourceData(d) @@ -59,18 +111,37 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro } flag := ldapi.FeatureFlagBody{ - Name: flagName, - Key: key, - Description: description, - Variations: variations, - Temporary: temporary, - Tags: tags, - IncludeInSnippet: includeInSnippet, - Defaults: defaults, + Name: flagName, + Key: key, + Description: &description, + Variations: &variations, + Temporary: &temporary, + Tags: &tags, + Defaults: defaults, } + if clientSideAvailabilityOk { + flag.ClientSideAvailability = clientSideAvailability + } else if includeInSnippetOk { + // If includeInSnippet is set, still use clientSideAvailability behind the scenes in order to switch UsingMobileKey to false if needed + flag.ClientSideAvailability = &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: includeInSnippet, + UsingMobileKey: false, + } + } else { + // If neither value is set, we should get the default from the project level and apply that + // IncludeInSnippetdefault is the same as defaultCSA.UsingEnvironmentId, so we can _ it + defaultCSA, _, err := getProjectDefaultCSAandIncludeInSnippet(client, projectKey) + if err != nil { + return fmt.Errorf("failed to get project level client side availability defaults. %v", err) + } + flag.ClientSideAvailability = &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: *defaultCSA.UsingEnvironmentId, + UsingMobileKey: *defaultCSA.UsingMobileKey, + } + } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, projectKey, flag, nil) + return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, projectKey).FeatureFlagBody(flag).Execute() }) if err != nil { @@ -82,7 +153,7 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro err = resourceFeatureFlagUpdate(d, metaRaw) if err != nil { // if there was a problem in the update state, we need to clean up completely by deleting the flag - _, deleteErr := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key) + _, deleteErr := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() if deleteErr != nil { return fmt.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -106,22 +177,54 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro name := d.Get(NAME).(string) tags := stringsFromResourceData(d, TAGS) includeInSnippet := d.Get(INCLUDE_IN_SNIPPET).(bool) + + snippetHasChange := d.HasChange(INCLUDE_IN_SNIPPET) + clientSideHasChange := d.HasChange(CLIENT_SIDE_AVAILABILITY) + // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk + // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) + _, clientSideAvailabilityOk := d.GetOk(CLIENT_SIDE_AVAILABILITY) temporary := d.Get(TEMPORARY).(bool) customProperties := customPropertiesFromResourceData(d) archived := d.Get(ARCHIVED).(bool) + clientSideAvailability := &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: d.Get("client_side_availability.0.using_environment_id").(bool), + UsingMobileKey: d.Get("client_side_availability.0.using_mobile_key").(bool), + } - patch := ldapi.PatchComment{ - Comment: "Terraform", + comment := "Terraform" + patch := ldapi.PatchWithComment{ + Comment: &comment, Patch: []ldapi.PatchOperation{ patchReplace("/name", name), patchReplace("/description", description), patchReplace("/tags", tags), - patchReplace("/includeInSnippet", includeInSnippet), patchReplace("/temporary", temporary), patchReplace("/customProperties", customProperties), patchReplace("/archived", archived), }} + if clientSideAvailabilityOk && clientSideHasChange { + patch.Patch = append(patch.Patch, patchReplace("/clientSideAvailability", clientSideAvailability)) + } else if includeInSnippetOk && snippetHasChange { + // If includeInSnippet is set, still use clientSideAvailability behind the scenes in order to switch UsingMobileKey to false if needed + patch.Patch = append(patch.Patch, patchReplace("/clientSideAvailability", &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: includeInSnippet, + UsingMobileKey: false, + })) + } else { + // If the user doesn't set either CSA or IIS in config, we pull the defaults from their Project level settings and apply those + // IncludeInSnippetdefault is the same as defaultCSA.UsingEnvironmentId, so we can _ it + defaultCSA, _, err := getProjectDefaultCSAandIncludeInSnippet(client, projectKey) + if err != nil { + return fmt.Errorf("failed to get project level client side availability defaults. %v", err) + } + patch.Patch = append(patch.Patch, patchReplace("/clientSideAvailability", &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: *defaultCSA.UsingEnvironmentId, + UsingMobileKey: *defaultCSA.UsingMobileKey, + })) + } + variationPatches, err := variationPatchesFromResourceData(d) if err != nil { return fmt.Errorf("failed to build variation patches. %v", err) @@ -145,7 +248,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key, patch) + return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(*&patch).Execute() }) }) @@ -162,7 +265,7 @@ func resourceFeatureFlagDelete(d *schema.ResourceData, metaRaw interface{}) erro key := d.Get(KEY).(string) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key) + res, err := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() return nil, res, err }) if err != nil { @@ -177,7 +280,7 @@ func resourceFeatureFlagExists(d *schema.ResourceData, metaRaw interface{}) (boo projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) - _, res, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key, nil) + _, res, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment.go b/launchdarkly/resource_launchdarkly_feature_flag_environment.go index a42d37f1..c5a34fcf 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceFeatureFlagEnvironment() *schema.Resource { @@ -105,15 +105,16 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf patches = append(patches, patchReplace(patchFlagEnvPath(d, "fallthrough"), fall)) if len(patches) > 0 { - patch := ldapi.PatchComment{ - Comment: "Terraform", + comment := "Terraform" + patch := ldapi.PatchWithComment{ + Comment: &comment, Patch: patches, } log.Printf("[DEBUG] %+v\n", patch) _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey, patch) + return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() }) }) if err != nil { @@ -167,8 +168,9 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf } offVariation := d.Get(OFF_VARIATION) - patch := ldapi.PatchComment{ - Comment: "Terraform", + comment := "Terraform" + patch := ldapi.PatchWithComment{ + Comment: &comment, Patch: []ldapi.PatchOperation{ patchReplace(patchFlagEnvPath(d, "on"), on), patchReplace(patchFlagEnvPath(d, "rules"), rules), @@ -182,7 +184,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf log.Printf("[DEBUG] %+v\n", patch) _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey, patch) + return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() }) }) if err != nil { @@ -214,7 +216,7 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf return fmt.Errorf("failed to find environment with key %q", envKey) } - flag, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey, nil) + flag, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } @@ -222,8 +224,9 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf // Set off variation to match default with how a rule is created offVariation := len(flag.Variations) - 1 - patch := ldapi.PatchComment{ - Comment: "Terraform", + comment := "Terraform" + patch := ldapi.PatchWithComment{ + Comment: &comment, Patch: []ldapi.PatchOperation{ patchReplace(patchFlagEnvPath(d, "on"), false), patchReplace(patchFlagEnvPath(d, "rules"), []ldapi.Rule{}), @@ -237,7 +240,7 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey, patch) + return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() }) }) if err != nil { diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go index 28be97b8..cdeaf12f 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go @@ -5,12 +5,9 @@ import ( "regexp" "testing" - "github.com/antihax/optional" "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" - - ldapi "github.com/launchdarkly/api-client-go" ) const ( @@ -677,7 +674,7 @@ func testAccCheckFeatureFlagEnvironmentExists(resourceName string) resource.Test return fmt.Errorf("environent key not found: %s", resourceName) } client := testAccProvider.Meta().(*Client) - _, _, err = client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projKey, flagKey, &ldapi.FeatureFlagsApiGetFeatureFlagOpts{Env: optional.NewInterface(envKey)}) + _, _, err = client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projKey, flagKey).Env(envKey).Execute() if err != nil { return fmt.Errorf("received an error getting feature flag environment. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index 44768c8a..92ba1c86 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -402,6 +402,56 @@ resource "launchdarkly_feature_flag" "empty_string_variation" { value = "non-empty" } } +` + testAccFeatureFlagIncludeInSnippet = ` +resource "launchdarkly_feature_flag" "sdk_settings" { + project_key = launchdarkly_project.test.key + key = "basic-flag-sdk-settings" + name = "Basic feature flag" + variation_type = "boolean" + include_in_snippet = true +} +` + testAccFeatureFlagIncludeInSnippetUpdate = ` +resource "launchdarkly_feature_flag" "sdk_settings" { + project_key = launchdarkly_project.test.key + key = "basic-flag-sdk-settings" + name = "Basic feature flag" + variation_type = "boolean" + include_in_snippet = false +} +` + testAccFeatureFlagIncludeInSnippetEmpty = ` +resource "launchdarkly_feature_flag" "sdk_settings" { + project_key = launchdarkly_project.test.key + key = "basic-flag-sdk-settings" + name = "Basic feature flag" + variation_type = "boolean" +} +` + testAccFeatureFlagClientSideAvailability = ` +resource "launchdarkly_feature_flag" "sdk_settings" { + project_key = launchdarkly_project.test.key + key = "basic-flag-sdk-settings" + name = "Basic feature flag" + variation_type = "boolean" + client_side_availability { + using_environment_id = true + using_mobile_key = true + } +} +` + testAccFeatureFlagClientSideAvailabilityUpdate = ` +resource "launchdarkly_feature_flag" "sdk_settings" { + project_key = launchdarkly_project.test.key + key = "basic-flag-sdk-settings" + name = "Basic feature flag" + variation_type = "boolean" + client_side_availability { + using_environment_id = false + using_mobile_key = false + } +} ` ) @@ -423,6 +473,25 @@ func withRandomProject(randomProject, resource string) string { %s`, randomProject, resource) } +func withRandomProjectIncludeInSnippetTrue(randomProject, resource string) string { + return fmt.Sprintf(` + resource "launchdarkly_project" "test" { + lifecycle { + ignore_changes = [environments] + } + include_in_snippet = true + name = "testProject" + key = "%s" + environments { + name = "testEnvironment" + key = "test" + color = "000000" + } + } + + %s`, randomProject, resource) +} + func TestAccFeatureFlag_Basic(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_feature_flag.basic" @@ -448,9 +517,10 @@ func TestAccFeatureFlag_Basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + // TODO: While we have to account for usingMobileKey being set to true by default, we cant use importStateVerify + // ImportStateVerify: true, }, }, }) @@ -521,9 +591,10 @@ func TestAccFeatureFlag_Number(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + // TODO: While we have to account for usingMobileKey being set to true by default, we cant use importStateVerify + // ImportStateVerify: true, }, }, }) @@ -874,9 +945,10 @@ func TestAccFeatureFlag_UpdateDefaults(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + // TODO: While we have to account for usingMobileKey being set to true by default, we cant use importStateVerify + // ImportStateVerify: true, }, }, }) @@ -919,9 +991,10 @@ func TestAccFeatureFlag_UpdateMultivariateDefaults(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + // TODO: While we have to account for usingMobileKey being set to true by default, we cant use importStateVerify + // ImportStateVerify: true, }, }, }) @@ -958,6 +1031,216 @@ func TestAccFeatureFlag_EmptyStringVariation(t *testing.T) { }) } +func TestAccFeatureFlag_ClientSideAvailabilityUpdate(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag.sdk_settings" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccFeatureFlagClientSideAvailability), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagClientSideAvailabilityUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), + ), + }, + }, + }) +} + +func TestAccFeatureFlag_IncludeInSnippetToClientSide(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag.sdk_settings" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccFeatureFlagIncludeInSnippet), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagClientSideAvailability), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagClientSideAvailabilityUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + ), + }, + }, + }) +} + +func TestAccFeatureFlag_ClientSideToIncludeInSnippet(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag.sdk_settings" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, testAccFeatureFlagClientSideAvailability), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + ), + }, + { + Config: withRandomProject(projectKey, testAccFeatureFlagIncludeInSnippetUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "boolean"), + resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), + resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), + resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), + resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + ), + }, + }, + }) +} + +func TestAccFeatureFlag_IncludeInSnippetRevertToDefault(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_feature_flag.sdk_settings" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create without value set and check for default value + { + Config: withRandomProjectIncludeInSnippetTrue(projectKey, testAccFeatureFlagIncludeInSnippetEmpty), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + ), + }, + // Replace default value with specific value + { + Config: withRandomProjectIncludeInSnippetTrue(projectKey, testAccFeatureFlagIncludeInSnippetUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + ), + }, + // Clear specific value, check for default + { + Config: withRandomProjectIncludeInSnippetTrue(projectKey, testAccFeatureFlagIncludeInSnippetEmpty), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFeatureFlagExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + ), + }, + }, + }) +} + func testAccCheckFeatureFlagExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -973,7 +1256,7 @@ func testAccCheckFeatureFlagExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("project key not found: %s", resourceName) } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projKey, flagKey, nil) + _, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projKey, flagKey).Execute() if err != nil { return fmt.Errorf("received an error getting feature flag. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index f3442a87..c2a8bcef 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -7,7 +7,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceProject() *schema.Resource { @@ -62,17 +62,17 @@ func resourceProjectCreate(d *schema.ResourceData, metaRaw interface{}) error { envs := environmentPostsFromResourceData(d) d.SetId(projectKey) - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: name, Key: projectKey, } if len(envs) > 0 { - projectBody.Environments = envs + projectBody.Environments = &envs } _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PostProject(client.ctx, projectBody) + return client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() }) if err != nil { return fmt.Errorf("failed to create project with name %s and projectKey %s: %v", name, projectKey, handleLdapiErr(err)) @@ -105,7 +105,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PatchProject(client.ctx, projectKey, patch) + return client.ld.ProjectsApi.PatchProject(client.ctx, projectKey).PatchOperation(patch).Execute() }) }) if err != nil { @@ -115,7 +115,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { oldSchemaEnvList, newSchemaEnvList := d.GetChange(ENVIRONMENTS) // Get the project so we can see if we need to create any environments or just update existing environments rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey) + return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() }) if err != nil { return fmt.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) @@ -142,7 +142,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { if !exists { envPost := environmentPostFromResourceData(env) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey, envPost) + return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() }) if err != nil { return fmt.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) @@ -154,13 +154,13 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { oldEnvConfig = rawOldConfig } // by default patching an env that was not recently tracked in the state will import it into the tf state - patches, err := getEnvironmentUpdatePatches(oldEnvConfig, envConfig) + patch, err := getEnvironmentUpdatePatches(oldEnvConfig, envConfig) if err != nil { return err } _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey, patches) + return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey).PatchOperation(patch).Execute() }) }) if err != nil { @@ -175,7 +175,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { envKey := envConfig[KEY].(string) if _, persists := envConfigsForCompare[envKey]; !persists { _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey) + res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey).Execute() return nil, res, err }) if err != nil { @@ -192,7 +192,7 @@ func resourceProjectDelete(d *schema.ResourceData, metaRaw interface{}) error { projectKey := d.Get(KEY).(string) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey) + res, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey).Execute() return nil, res, err }) @@ -209,7 +209,7 @@ func resourceProjectExists(d *schema.ResourceData, metaRaw interface{}) (bool, e func projectExists(projectKey string, meta *Client) (bool, error) { _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.ProjectsApi.GetProject(meta.ctx, projectKey) + return meta.ld.ProjectsApi.GetProject(meta.ctx, projectKey).Execute() }) if isStatusNotFound(res) { log.Println("got 404 when getting project. returning false.") diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index 5daceaff..b98ff972 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -364,7 +364,7 @@ func testAccCheckProjectExists(resourceName string) resource.TestCheckFunc { } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.ProjectsApi.GetProject(client.ctx, rs.Primary.ID) + _, _, err := client.ld.ProjectsApi.GetProject(client.ctx, rs.Primary.ID).Execute() if err != nil { return fmt.Errorf("received an error getting project. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_segment.go b/launchdarkly/resource_launchdarkly_segment.go index 274ccf56..d8a3bd2d 100644 --- a/launchdarkly/resource_launchdarkly_segment.go +++ b/launchdarkly/resource_launchdarkly_segment.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceSegment() *schema.Resource { @@ -62,15 +62,15 @@ func resourceSegmentCreate(d *schema.ResourceData, metaRaw interface{}) error { segmentName := d.Get(NAME).(string) tags := stringsFromResourceData(d, TAGS) - segment := ldapi.UserSegmentBody{ + segment := ldapi.SegmentBody{ Name: segmentName, Key: key, - Description: description, - Tags: tags, + Description: &description, + Tags: &tags, } _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.PostUserSegment(client.ctx, projectKey, envKey, segment) + return client.ld.SegmentsApi.PostSegment(client.ctx, projectKey, envKey).SegmentBody(segment).Execute() }) if err != nil { @@ -107,19 +107,22 @@ func resourceSegmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { if err != nil { return err } - patch := []ldapi.PatchOperation{ - patchReplace("/name", name), - patchReplace("/description", description), - patchReplace("/tags", tags), - patchReplace("/temporary", TEMPORARY), - patchReplace("/included", included), - patchReplace("/excluded", excluded), - patchReplace("/rules", rules), - } + comment := "Terraform" + patch := ldapi.PatchWithComment{ + Comment: &comment, + Patch: []ldapi.PatchOperation{ + patchReplace("/name", name), + patchReplace("/description", description), + patchReplace("/tags", tags), + patchReplace("/temporary", TEMPORARY), + patchReplace("/included", included), + patchReplace("/excluded", excluded), + patchReplace("/rules", rules), + }} _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.PatchUserSegment(client.ctx, projectKey, envKey, key, patch) + return client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, key).PatchWithComment(patch).Execute() }) }) if err != nil { @@ -136,7 +139,7 @@ func resourceSegmentDelete(d *schema.ResourceData, metaRaw interface{}) error { key := d.Get(KEY).(string) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.UserSegmentsApi.DeleteUserSegment(client.ctx, projectKey, envKey, key) + res, err := client.ld.SegmentsApi.DeleteSegment(client.ctx, projectKey, envKey, key).Execute() return nil, res, err }) @@ -154,7 +157,7 @@ func resourceSegmentExists(d *schema.ResourceData, metaRaw interface{}) (bool, e key := d.Get(KEY).(string) _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.GetUserSegment(client.ctx, projectKey, envKey, key) + return client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, key).Execute() }) if isStatusNotFound(res) { return false, nil diff --git a/launchdarkly/resource_launchdarkly_segment_test.go b/launchdarkly/resource_launchdarkly_segment_test.go index 2d3cde1f..4db7d684 100644 --- a/launchdarkly/resource_launchdarkly_segment_test.go +++ b/launchdarkly/resource_launchdarkly_segment_test.go @@ -291,7 +291,7 @@ func testAccCheckSegmentExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("project key not found: %s", resourceName) } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.UserSegmentsApi.GetUserSegment(client.ctx, projKey, envKey, segmentKey) + _, _, err := client.ld.SegmentsApi.GetSegment(client.ctx, projKey, envKey, segmentKey).Execute() if err != nil { return fmt.Errorf("received an error getting environment. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_team_member.go b/launchdarkly/resource_launchdarkly_team_member.go index 2d9bb00f..9fd93b15 100644 --- a/launchdarkly/resource_launchdarkly_team_member.go +++ b/launchdarkly/resource_launchdarkly_team_member.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceTeamMember() *schema.Resource { @@ -65,7 +65,7 @@ func resourceTeamMemberCreate(d *schema.ResourceData, metaRaw interface{}) error memberEmail := d.Get(EMAIL).(string) firstName := d.Get(FIRST_NAME).(string) lastName := d.Get(LAST_NAME).(string) - memberRole := ldapi.Role(d.Get(ROLE).(string)) + memberRole := d.Get(ROLE).(string) customRolesRaw := d.Get(CUSTOM_ROLES).(*schema.Set).List() customRoles := make([]string, len(customRolesRaw)) @@ -73,16 +73,16 @@ func resourceTeamMemberCreate(d *schema.ResourceData, metaRaw interface{}) error customRoles[i] = cr.(string) } - membersBody := ldapi.MembersBody{ + membersBody := ldapi.NewMemberForm{ Email: memberEmail, - FirstName: firstName, - LastName: lastName, + FirstName: &firstName, + LastName: &lastName, Role: &memberRole, - CustomRoles: customRoles, + CustomRoles: &customRoles, } membersRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.TeamMembersApi.PostMembers(client.ctx, []ldapi.MembersBody{membersBody}) + return client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm([]ldapi.NewMemberForm{membersBody}).Execute() }) members := membersRaw.(ldapi.Members) if err != nil { @@ -98,7 +98,7 @@ func resourceTeamMemberRead(d *schema.ResourceData, metaRaw interface{}) error { memberID := d.Id() memberRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.TeamMembersApi.GetMember(client.ctx, memberID) + return client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Execute() }) member := memberRaw.(ldapi.Member) if isStatusNotFound(res) { @@ -150,7 +150,7 @@ func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.TeamMembersApi.PatchMember(client.ctx, memberID, patch) + return client.ld.AccountMembersApi.PatchMember(client.ctx, memberID).PatchOperation(patch).Execute() }) }) if err != nil { @@ -164,7 +164,7 @@ func resourceTeamMemberDelete(d *schema.ResourceData, metaRaw interface{}) error client := metaRaw.(*Client) _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.TeamMembersApi.DeleteMember(client.ctx, d.Id()) + res, err := client.ld.AccountMembersApi.DeleteMember(client.ctx, d.Id()).Execute() return nil, res, err }) if err != nil { @@ -179,7 +179,7 @@ func resourceTeamMemberExists(d *schema.ResourceData, metaRaw interface{}) (bool } func teamMemberExists(memberID string, meta *Client) (bool, error) { - _, res, err := meta.ld.TeamMembersApi.GetMember(meta.ctx, memberID) + _, res, err := meta.ld.AccountMembersApi.GetMember(meta.ctx, memberID).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_team_member_test.go b/launchdarkly/resource_launchdarkly_team_member_test.go index 87acbdf5..8bfec216 100644 --- a/launchdarkly/resource_launchdarkly_team_member_test.go +++ b/launchdarkly/resource_launchdarkly_team_member_test.go @@ -237,7 +237,7 @@ func testAccCheckMemberExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("team member ID is not set") } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.TeamMembersApi.GetMember(client.ctx, rs.Primary.ID) + _, _, err := client.ld.AccountMembersApi.GetMember(client.ctx, rs.Primary.ID).Execute() if err != nil { return fmt.Errorf("received an error getting team member. %s", err) } diff --git a/launchdarkly/resource_launchdarkly_webhook.go b/launchdarkly/resource_launchdarkly_webhook.go index 32407630..06067fd3 100644 --- a/launchdarkly/resource_launchdarkly_webhook.go +++ b/launchdarkly/resource_launchdarkly_webhook.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func resourceWebhook() *schema.Resource { @@ -41,29 +41,32 @@ func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { webhookURL := d.Get(URL).(string) webhookSecret := d.Get(SECRET).(string) webhookName := d.Get(NAME).(string) - statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) - if err != nil { - return err - } webhookOn := d.Get(ON).(bool) - webhookBody := ldapi.WebhookBody{ - Url: webhookURL, - Secret: webhookSecret, - On: webhookOn, - Name: webhookName, - Statements: statements, + webhookBody := ldapi.WebhookPost{ + Url: webhookURL, + On: webhookOn, + Name: &webhookName, + } + + if rawStatements, ok := d.GetOk(STATEMENTS); ok { + statements, err := policyStatementsFromResourceData(rawStatements.([]interface{})) + if err != nil { + return err + } + webhookBody.Statements = &statements } // The sign field isn't returned when GETting a webhook so terraform can't import it properly. // We hide the field from terraform to avoid import problems. if webhookSecret != "" { + webhookBody.Secret = &webhookSecret webhookBody.Sign = true } webhookRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PostWebhook(client.ctx, webhookBody) + return client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() }) webhook := webhookRaw.(ldapi.Webhook) if err != nil { @@ -117,7 +120,7 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PatchWebhook(client.ctx, webhookID, patch) + return client.ld.WebhooksApi.PatchWebhook(client.ctx, webhookID).PatchOperation(patch).Execute() }) }) if err != nil { @@ -132,7 +135,7 @@ func resourceWebhookDelete(d *schema.ResourceData, metaRaw interface{}) error { webhookID := d.Id() _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookID) + res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookID).Execute() return nil, res, err }) @@ -149,7 +152,7 @@ func resourceWebhookExists(d *schema.ResourceData, metaRaw interface{}) (bool, e func webhookExists(webhookID string, meta *Client) (bool, error) { _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.WebhooksApi.GetWebhook(meta.ctx, webhookID) + return meta.ld.WebhooksApi.GetWebhook(meta.ctx, webhookID).Execute() }) if isStatusNotFound(res) { return false, nil diff --git a/launchdarkly/resource_launchdarkly_webhook_test.go b/launchdarkly/resource_launchdarkly_webhook_test.go index 34a12d9e..b88783a3 100644 --- a/launchdarkly/resource_launchdarkly_webhook_test.go +++ b/launchdarkly/resource_launchdarkly_webhook_test.go @@ -362,7 +362,7 @@ func testAccCheckWebhookExists(resourceName string) resource.TestCheckFunc { return fmt.Errorf("webhook ID is not set") } client := testAccProvider.Meta().(*Client) - _, _, err := client.ld.WebhooksApi.GetWebhook(client.ctx, rs.Primary.ID) + _, _, err := client.ld.WebhooksApi.GetWebhook(client.ctx, rs.Primary.ID).Execute() if err != nil { return fmt.Errorf("received an error getting webhook. %s", err) } diff --git a/launchdarkly/rollout_helper.go b/launchdarkly/rollout_helper.go index 09a30b33..69f577f3 100644 --- a/launchdarkly/rollout_helper.go +++ b/launchdarkly/rollout_helper.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func rolloutSchema() *schema.Schema { diff --git a/launchdarkly/rule_helper.go b/launchdarkly/rule_helper.go index 15256a6c..702ad918 100644 --- a/launchdarkly/rule_helper.go +++ b/launchdarkly/rule_helper.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func rulesSchema() *schema.Schema { @@ -69,7 +69,7 @@ func ruleFromResourceData(val interface{}) (rule, error) { if len(rolloutFromResourceData(ruleMap[ROLLOUT_WEIGHTS]).Variations) > 0 { r.Rollout = rolloutFromResourceData(ruleMap[ROLLOUT_WEIGHTS]) if bucketByFound { - r.Rollout.BucketBy = bucketBy + r.Rollout.BucketBy = &bucketBy } } else { if bucketByFound && bucketBy != "" { diff --git a/launchdarkly/segment_rule_helper.go b/launchdarkly/segment_rule_helper.go index 232d4936..55c2d04e 100644 --- a/launchdarkly/segment_rule_helper.go +++ b/launchdarkly/segment_rule_helper.go @@ -3,7 +3,7 @@ package launchdarkly import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func segmentRulesSchema() *schema.Schema { @@ -47,9 +47,11 @@ func segmentRulesFromResourceData(d *schema.ResourceData, metaRaw interface{}) ( func segmentRuleFromResourceData(val interface{}) (ldapi.UserSegmentRule, error) { ruleMap := val.(map[string]interface{}) + weight := int32(ruleMap[WEIGHT].(int)) + bucketBy := ruleMap[BUCKET_BY].(string) r := ldapi.UserSegmentRule{ - Weight: int32(ruleMap[WEIGHT].(int)), - BucketBy: ruleMap[BUCKET_BY].(string), + Weight: &weight, + BucketBy: &bucketBy, } for _, c := range ruleMap[CLAUSES].([]interface{}) { clause, err := clauseFromResourceData(c) diff --git a/launchdarkly/segments_helper.go b/launchdarkly/segments_helper.go index 97d89926..289aa89c 100644 --- a/launchdarkly/segments_helper.go +++ b/launchdarkly/segments_helper.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseSegmentSchema() map[string]*schema.Schema { @@ -45,7 +45,7 @@ func segmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) err segmentKey := d.Get(KEY).(string) segmentRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.UserSegmentsApi.GetUserSegment(client.ctx, projectKey, envKey, segmentKey) + return client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, segmentKey).Execute() }) segment := segmentRaw.(ldapi.UserSegment) if isStatusNotFound(res) && !isDataSource { diff --git a/launchdarkly/target_helper.go b/launchdarkly/target_helper.go index 36f3e829..c973a6cc 100644 --- a/launchdarkly/target_helper.go +++ b/launchdarkly/target_helper.go @@ -4,7 +4,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func targetsSchema() *schema.Schema { diff --git a/launchdarkly/target_helper_test.go b/launchdarkly/target_helper_test.go index d94c8268..24802ab1 100644 --- a/launchdarkly/target_helper_test.go +++ b/launchdarkly/target_helper_test.go @@ -3,7 +3,7 @@ package launchdarkly import ( "testing" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" ) diff --git a/launchdarkly/team_member_helper.go b/launchdarkly/team_member_helper.go index 80a38c32..45485c2d 100644 --- a/launchdarkly/team_member_helper.go +++ b/launchdarkly/team_member_helper.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // The LD api returns custom role IDs (not keys). Since we want to set custom_roles with keys, we need to look up their IDs @@ -12,7 +12,7 @@ func customRoleIDsToKeys(client *Client, ids []string) ([]string, error) { customRoleKeys := make([]string, 0, len(ids)) for _, customRoleID := range ids { roleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID) + return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() }) role := roleRaw.(ldapi.CustomRole) if isStatusNotFound(res) { @@ -31,7 +31,7 @@ func customRoleKeysToIDs(client *Client, keys []string) ([]string, error) { customRoleIds := make([]string, 0, len(keys)) for _, key := range keys { roleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, key) + return client.ld.CustomRolesApi.GetCustomRole(client.ctx, key).Execute() }) role := roleRaw.(ldapi.CustomRole) if isStatusNotFound(res) { diff --git a/launchdarkly/test_utils.go b/launchdarkly/test_utils.go index 17cd73cb..38b6ca3b 100644 --- a/launchdarkly/test_utils.go +++ b/launchdarkly/test_utils.go @@ -4,13 +4,13 @@ import ( "fmt" "net/http" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) // testAccDataSourceProjectCreate creates a project with the given project parameters -func testAccDataSourceProjectCreate(client *Client, projectBody ldapi.ProjectBody) (*ldapi.Project, error) { +func testAccDataSourceProjectCreate(client *Client, projectBody ldapi.ProjectPost) (*ldapi.Project, error) { project, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PostProject(client.ctx, projectBody) + return client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() }) if err != nil { return nil, err @@ -22,7 +22,7 @@ func testAccDataSourceProjectCreate(client *Client, projectBody ldapi.ProjectBod } func testAccDataSourceProjectDelete(client *Client, projectKey string) error { - _, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey) + _, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey).Execute() if err != nil { return err } @@ -30,7 +30,7 @@ func testAccDataSourceProjectDelete(client *Client, projectKey string) error { } func testAccDataSourceFeatureFlagScaffold(client *Client, projectKey string, flagBody ldapi.FeatureFlagBody) (*ldapi.FeatureFlag, error) { - projectBody := ldapi.ProjectBody{ + projectBody := ldapi.ProjectPost{ Name: "Flag Test Project", Key: projectKey, } @@ -40,7 +40,7 @@ func testAccDataSourceFeatureFlagScaffold(client *Client, projectKey string, fla } flag, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, project.Key, flagBody, nil) + return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, project.Key).FeatureFlagBody(flagBody).Execute() }) if err != nil { return nil, err diff --git a/launchdarkly/variations_helper.go b/launchdarkly/variations_helper.go index f5d9c75e..5fcf6e27 100644 --- a/launchdarkly/variations_helper.go +++ b/launchdarkly/variations_helper.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) const ( @@ -168,30 +168,38 @@ func variationsFromResourceData(d *schema.ResourceData) ([]ldapi.Variation, erro func boolVariationFromResourceData(variation interface{}) ldapi.Variation { variationMap := variation.(map[string]interface{}) v := variationMap[VALUE].(string) == "true" - return ldapi.Variation{ - Name: variationMap[NAME].(string), - Description: variationMap[DESCRIPTION].(string), - Value: ptr(v), + transformed := ldapi.Variation{ + Value: ptr(v), } + name := variationMap[NAME].(string) + if name != "" { + transformed.Name = &name + } + description := variationMap[DESCRIPTION].(string) + if description != "" { + transformed.Description = &description + } + return transformed } func stringVariationFromResourceData(variation interface{}) ldapi.Variation { - var v interface{} + var transformed ldapi.Variation if variation == nil { // handle empty string value - v = "" - return ldapi.Variation{ - Name: "", - Description: "", - Value: &v, - } + transformed.Value = strPtr("") + return transformed } variationMap := variation.(map[string]interface{}) - v = variationMap[VALUE] - return ldapi.Variation{ - Name: variationMap[NAME].(string), - Description: variationMap[DESCRIPTION].(string), - Value: &v, + v := variationMap[VALUE] + transformed.Value = &v + name := variationMap[NAME].(string) + if name != "" { + transformed.Name = &name + } + description := variationMap[DESCRIPTION].(string) + if description != "" { + transformed.Description = &description } + return transformed } func numberVariationFromResourceData(variation interface{}) (ldapi.Variation, error) { @@ -201,11 +209,16 @@ func numberVariationFromResourceData(variation interface{}) (ldapi.Variation, er if err != nil { return ldapi.Variation{}, fmt.Errorf("%q is an invalid number variation value. %v", stringValue, err) } - return ldapi.Variation{ - Name: variationMap[NAME].(string), - Description: variationMap[DESCRIPTION].(string), - Value: ptr(v), - }, nil + transformed := ldapi.Variation{Value: ptr(v)} + name := variationMap[NAME].(string) + if name != "" { + transformed.Name = &name + } + description := variationMap[DESCRIPTION].(string) + if description != "" { + transformed.Description = &description + } + return transformed, nil } func jsonVariationFromResourceData(variation interface{}) (ldapi.Variation, error) { @@ -216,11 +229,16 @@ func jsonVariationFromResourceData(variation interface{}) (ldapi.Variation, erro if err != nil { return ldapi.Variation{}, fmt.Errorf("%q is an invalid json variation value. %v", stringValue, err) } - return ldapi.Variation{ - Name: variationMap[NAME].(string), - Description: variationMap[DESCRIPTION].(string), - Value: ptr(v), - }, nil + transformed := ldapi.Variation{Value: ptr(v)} + name := variationMap[NAME].(string) + if name != "" { + transformed.Name = &name + } + description := variationMap[DESCRIPTION].(string) + if description != "" { + transformed.Description = &description + } + return transformed, nil } func stringifyValue(value interface{}) string { @@ -257,7 +275,7 @@ func variationsToResourceData(variations []ldapi.Variation, variationType string transformed := make([]interface{}, 0, len(variations)) for _, variation := range variations { - v, err := variationValueToString(variation.Value, variationType) + v, err := variationValueToString(&variation.Value, variationType) if err != nil { return nil, err } @@ -273,11 +291,10 @@ func variationsToResourceData(variations []ldapi.Variation, variationType string func variationsToVariationType(variations []ldapi.Variation) (string, error) { // since all variations have a uniform type, checking the first variation is sufficient - valPtr := variations[0].Value - if valPtr == nil { - return "", fmt.Errorf("nil variation value: %v", valPtr) + variationValue := variations[0].Value + if variationValue == nil { + return "", fmt.Errorf("nil variation value: %v", variationValue) } - variationValue := *valPtr var variationType string switch variationValue.(type) { case bool: diff --git a/launchdarkly/variations_helper_test.go b/launchdarkly/variations_helper_test.go index 68f0dec4..9546dd24 100644 --- a/launchdarkly/variations_helper_test.go +++ b/launchdarkly/variations_helper_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -32,8 +32,8 @@ func TestVariationsFromResourceData(t *testing.T) { }, }}, expected: []ldapi.Variation{ - {Name: "nameValue", Description: "descValue", Value: ptr("a string value")}, - {Name: "nameValue2", Description: "descValue2", Value: ptr("another string value")}, + {Name: strPtr("nameValue"), Description: strPtr("descValue"), Value: ptr("a string value")}, + {Name: strPtr("nameValue2"), Description: strPtr("descValue2"), Value: ptr("another string value")}, }, }, { @@ -110,7 +110,7 @@ func TestVariationsFromResourceData(t *testing.T) { for idx, expected := range tc.expected { assert.Equal(t, expected.Name, actualVariations[idx].Name) assert.Equal(t, expected.Description, actualVariations[idx].Description) - assert.Equal(t, *expected.Value, *actualVariations[idx].Value) + assert.Equal(t, expected.Value, actualVariations[idx].Value) } }) } diff --git a/launchdarkly/webhooks_helper.go b/launchdarkly/webhooks_helper.go index 85b74069..02a28b07 100644 --- a/launchdarkly/webhooks_helper.go +++ b/launchdarkly/webhooks_helper.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseWebhookSchema() map[string]*schema.Schema { @@ -37,7 +37,7 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er } webhookRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.GetWebhook(client.ctx, webhookID) + return client.ld.WebhooksApi.GetWebhook(client.ctx, webhookID).Execute() }) webhook := webhookRaw.(ldapi.Webhook) if isStatusNotFound(res) && !isDataSource { @@ -48,7 +48,13 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er if err != nil { return fmt.Errorf("failed to get webhook with id %q: %s", webhookID, handleLdapiErr(err)) } - statements := policyStatementsToResourceData(webhook.Statements) + if webhook.Statements != nil { + statements := policyStatementsToResourceData(*webhook.Statements) + err = d.Set(STATEMENTS, statements) + if err != nil { + return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) + } + } if isDataSource { d.SetId(webhook.Id) @@ -58,11 +64,6 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er _ = d.Set(ON, webhook.On) _ = d.Set(NAME, webhook.Name) - err = d.Set(STATEMENTS, statements) - if err != nil { - return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) - } - err = d.Set(TAGS, webhook.Tags) if err != nil { return fmt.Errorf("failed to set tags on webhook with id %q: %v", webhookID, err) diff --git a/website/docs/d/feature_flag.html.markdown b/website/docs/d/feature_flag.html.markdown index 43eec85c..f362c55d 100644 --- a/website/docs/d/feature_flag.html.markdown +++ b/website/docs/d/feature_flag.html.markdown @@ -48,6 +48,8 @@ In addition to the arguments above, the resource exports the following attribute - `temporary` - Whether the flag is a temporary flag. +- `include_in_snippet` - **Deprecated** A boolean describing whether this flag has been made available to the client-side Javescript SDK using the client-side ID only. `include_in_snippet` is now deprecated. Please retrieve information from `client_side_availability.using_environment_id` to maintain future compatability. + - `client_side_availability` - A map describing whether this flag has been made available to the client-side JavaScript SDK. To learn more, read [Nested Client-Side Availability Block](#nested-client-side-availability-block). - `custom_properties` - List of nested blocks describing the feature flag's [custom properties](https://docs.launchdarkly.com/docs/custom-properties). To learn more, read [Nested Custom Properties](#nested-custom-properties). diff --git a/website/docs/r/feature_flag.html.markdown b/website/docs/r/feature_flag.html.markdown index d88b17c9..7e4bdd0f 100644 --- a/website/docs/r/feature_flag.html.markdown +++ b/website/docs/r/feature_flag.html.markdown @@ -96,10 +96,13 @@ resource "launchdarkly_feature_flag" "json_example" { - `temporary` - (Optional) Specifies whether the flag is a temporary flag. -- `include_in_snippet` - (Optional) Specifies whether this flag should be made available to the client-side JavaScript SDK. +- `include_in_snippet` - **Deprecated** (Optional) Specifies whether this flag should be made available to the client-side JavaScript SDK using the client-side Id. This value gets its default from your project configuration if not set. `include_in_snippet` is now deprecated. Please migrate to `client_side_availability.using_environment_id` to maintain future compatability. + +- `client_side_availability` - (Optional) A block describing whether this flag should be made available to the client-side JavaScript SDK using the client-side Id, mobile key, or both. This value gets its default from your project configuration if not set. To learn more, read [Nested Client-Side Availability Block](#nested-client-side-availability-block). - `custom_properties` - (Optional) List of nested blocks describing the feature flag's [custom properties](https://docs.launchdarkly.com/docs/custom-properties). To learn more, read [Nested Custom Properties](#nested-custom-properties). + ### Nested Variations Blocks Nested `variations` blocks have the following structure: @@ -126,6 +129,14 @@ Nested `defaults` blocks have the following structure: - `off_variation` - (Required) The index of the variation the flag will default to in all new environments when off. +### Nested Client-Side Availibility Block + +The nested `client_side_availability` block has the following structure: + +- `using_environment_id` - (Optional) Whether this flag is available to SDKs using the client-side ID. + +- `using_mobile_key` - (Optional) Whether this flag is available to SDKs using a mobile key. + ### Nested Custom Properties Nested `custom_properties` have the following structure: From 5bf6965469a63c98c68cb10c87017858f764ccf6 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 23 Dec 2021 15:12:45 +0100 Subject: [PATCH 14/36] prepare 2.2.0 release (#75) (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update CHANGELOG.md fix error in changelog * Imiller/ch117193/rename flag_fallthrough to fallthrough & user_targets to targets (#131) * add fallthrough and deprecate message to flag_fallthrough * update doc * conflicts with * handle * update a test * update changelog * fix typos in docs * add targets attribute * fix linter complaint * handle in create * update * handle read * update commetn * update tests * hack remove user_targets from plan * set both computed attributes in read * update changelog * always set * simplify helper * fix data source test * add test for deprecated field updates * fix fromResourceData functions * check renamed on as well * Update data source d oc * update data source tests * Imiller/ch117375/GitHub issue empty string variation (#135) * should fix it * add test case * update changelog and doc * Fix duplicate text in CHANGELOG.md Somehow the changelog test was duplicated. Removing, and we can fix the changelog on our next release. * V2 main (#128) * destination on should default to false (v2) (#124) * remove enabled * test that on defaults to false when removed * Imiller/ch115579/fix include in snippet bug (#129) * all it needed was a default val * add test to ensure optional attributes get set correctly when removed * fix test * use TestCheckNoResourceAttr instead of TestCheckResourceAttr * update changelog * Imiller/ch113419/upgrade terraform client to v2 (#114) * run v2 helper * replace hashcode.String() with schema.HashString() * replace terraform.ResourceProvider with *schema.Provider * fix data source TypeMaps * bump v2 sdk to latest (#116) * merge * Remove versions.tf (#119) * Imiller/ch114162/upgrade resource launchdarkly environment (#121) * make project_key required * update descriptions * update changelog * Imiller/ch114165/v1 project audit (#120) * add descriptions * update doc dscription * audit team member for v1 (#122) * update descriptions * add import instructions to team member docs * add team member examples * ForceNew when email is updated * update test file structure to better reflect provider pattern * add AtLeastOneOf for role and custom_roles * oops wrong doc * use validation.StringInSlice() * refactor key conversion functions into helper file * remove owner * forgot changelog * Imiller/ch114163/v1 feature flag env (#112) * add descriptions for everything * fix dangling default values * fix bug where creating a non-bool flag with no variations creates a bool flag * add bug fixes to changelog * built in an idiotic bug, fixed now * see if this fixes all of the tests * found typo: * fix broken tests * add descriptions * update some descriptions for clarity * make clauses RequiredWith on variations to preempt api error * fix error in doc * fix docs properly * deprecated targeting_enabled in favor of on * remove ReuqiredWith because it's causing a bug * fix plan bug * hopefully this will fix it * ughhhhh it was that it needed to be computed * forgot the doc oops * forgot the changelog too meep * destination on should default to false (v2) (#124) * remove enabled * test that on defaults to false when removed * Imiller/ch114160/v1 custom role (#125) * add descriptions * reformat tests to match pattern * check removal of optional attributes removes * holy crap how did this not break earlier * not sure how that one stuck around either * somehow we were not actually setting the flag_id * fix feature flag read * fix csa on project read * apparently errors are different too * remove lingering merge conflict text from changelog * fix noMatchReturnsError errors * remove hash indices on type set * remove hash function for custom role policy * fix destination tests * fix feature flag tests * fix env tests * fix project tests * fix segment tests * fix segment tests and remove unused testAccTagKey function * revert computed * define CSA inline * update changelog * remove MaxItems property from data sources * fix forProject Co-authored-by: Henry Barrow * Imiller/ch115576/revert feature flag env optional attributes (#130) * remove deprecated attribute targeting_enabled (replaced by on) * on should default to false when removed * forgot to fix data source tests * fix test to handle on reversion * make rules not computed * make user_targets not computed * update comments * finally got fallthrough working * fix test * clarify * fix more tests * remove deprecated user_targets and flag_fallthrough from schema * update read * revert helpers * update create * update tests * update example * update doc * remove deprecated fields from keys * update changelog * fix fallthrough and fix test * update changelog * missed a bit on the docs' * make prerequisite not computed * udate changelog again * add test case for removing prereqs * update flag_id description for clarity * add defaults for optional fields * update test again * update changelog again * handle off_variation defaults * fix data source test * have to use GetOkExists instead of GetOk to be able to set 0 off_variation on create * remove commented-out code * Imiller/ch115576/make fallthrough off variation required (#134) * make required in schemas * fix some test configs * add required attributes to rest of tests. tests all failing * always set both on read * no need to check if fallthrough not set because tf will enforce * update CRU functions * fix data source tests * update changelog and docs for new required attributes * remove comment no longer applicable * fix typo * remove `enabled` on webhooks * removed `policy_statements` on webhooks * removed `targeting_enabled` on feature flag environment * update change log * Change schema function to use recommended one. * Imiller/ch119139/make env configs required on project resource (#137) * make required * delete envs that were previously in state and have been removed * remove redundant test & add required envs * ImportSTateVerifyIgnore envs * update docs * move full_config example into v1_full_config * add v2 full config * update example * update test project scaffolding function * change State to StateContext function * update comment * fix destination test bug * add test case for when we fix env defaults * add note to import about envs * update changelog * remove debug bin * add debug_bin to gitignore * address pr comments by adding back stuff to docs * update all example versions to 1.7 * Added logic to delete remote webhook statements when statements blockā€¦ (#133) * Added logic to delete remote webhook statements when statements block is removed * refactored tests * Added has changes check around the webhook policy statements * Reverted changes in version file * Removed check for deprecated policy_statements * Removed deprecated fields and refactored helper functions using those deprecated fields * Removed ConflictsWith for the webhook ON schema since ENABLED no longer exists in the schema * Fixed test cases * Updated changelog, refactored getWebhookOn and updated the docs * Updated version in example and added default config for webhook ON element * Imiller/ch119137/make segment clause negate field optional (#136) * make oiptional and default to false + update test * add example segment config for 2.0 * this should be v2.0.0 * Do not require fallthrough and off_variation in ff_env data source (#142) * added version to versions.tf file for webhook * v2 for custom_role and destination. Update read me * Change targets schema to set and add 'variation' attribute (#141) * Revise targets schema * update docs * Make target values required * Apply suggestions from code review Co-authored-by: Isabelle Miller * Change List to Set in description * remove debug_bin Co-authored-by: Isabelle Miller * Imiller/ch115572/restructure default flag variations (#146) * add new defaults to schema * update tests to expected configs / outcomes * add default on * restructure defaultVariationsFromResourceData * update test cases * hitting a non-empty plan error in test * ffs just needed to be Computed * update tests * update docs and changelog * fix test * lost a brace * fix variation helper * fix changelog * small doc fixes (#147) * remove enabled from destination docs * update webhook doc * update doc on name issue (#151) * environment and data_source_webhook examples (#149) * added more examples * added comments * Imiller/ch120560/feature flag maintainer id should be removed (#150) * make computed and update tes * update doc * update changelog * Imiller/ch119620/audit examples (#152) * make separate v1 and v2 feature flag examples * ensure v2 ff example works * move some stuff around * some more reorg * more reorg * update toc in readme * update v2 feature flag readme * Bug Fix: Revert optional env attributes to defaults (#148) * Fixed and tests passing * improve env update test * default_ttl default, and create new test * default_ttl should reset to 0 * update docs and changelog Co-authored-by: Isabelle Miller * remove test examples shouldn't have been checked in * add terraform version note to changelog Co-authored-by: Henry Barrow Co-authored-by: Sunny Co-authored-by: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> * Fix import of launchdarkly_project nested environments (#153) * start importing all environments * Preserve environment order * Update changelog * Add ignore_changes to withRandomProject * update doc (#154) * Imiller/sc 119920/add boolean archive attribute to launchdarkly (#155) * add archived to ff env schema * make it a global property * put it in the wrong schema doh * attempt archive before deleting so we can return an error if there are dependencies * prelim test * some syntax issues * gotta figure out error bit * remove failing test * forgot to set archived in read function * actually no need to archive first * Update index of docs with recommended version (#156) * Update index of docs with recommended version * Show how to configure the access token * Imiller/sc 123568/add approval settings to environment resource (#157) * prelim schema * tweak approval schema * draft conversion functions * i forgot you can't use conflictswith with nested attributes * a mess of things * fixed up patches * handle patch remove * okay i think this should handle all cases * unused transformation function since no setting * forgot to handle most obvious case * need to set but only sometimes * min num approvals cannot exceed 5 * make min num approvals required * project test * fix helper function for project - first pass * make entire approvals schema computed * projet tests working * NOW project tests working * remove dead code * update docs * update changelog * simplify approval helper function * switch old and new afunc arguments * Imiller/sc 126337/make can apply declined changes default to (#158) * update error message * change default in schema * update tests * update docs' * update changelog * clarify rollout weights (#159) * Upgrade Terraform provider to use api-client-go 7 (#160) * Start updating go client * Continue updating resources * feature flag resource * feature flag resource test * approval settings and custom role policies * destination resource * environment * feature flag environment * project * segment * team member * webhook * rule, segment rule, segments, and team member helpers * variations helper * data source tests * webhooks helper * resource tests * variations helper test * policy helpers and tests probably will all fail * custom role and access token probably also broken * policy statements helper update * instantiate config prior to setting other values on it * fix 401 * vendor v7 * update all imports to v7 * missed one * fix nil pointer dereference * instantiate with var instead of make to fix non-empty slice bu * custom roles now complains if you try to set more than one of Role, InlineRole, or CustomRoleIds * fix webhook statemetns * webhook read check if statements are nil & webhook create only set secret if defined * update changelog * fix webhook data source tests * new pointers in client FeatureFlagConfig breaking ff env data source tests * fix negative expiry thing (apparently the api no longer handles it so we have to) * move reset into helper function * hackily handle token reset requests until client bug is fixed * fix variation transformation - can no longer point to empty string * fix notResource and notActions * fix policy helper tests * fix the weird empty string value in variations issue * strip protocol from host if provided * go mod tidy && vendor * update all go packages * clean up commentsg * clean up comment * Ffeldberg/sc 130551/add the ability to adjust the sdks using (#161) * chore: first attempts * feat: updating flag resources works, reading/creating plan is a bit iffy * chore: clean up comments a bit * chore: add TODO as reminder * chore: add more todos * chore: use ConflictsWith to ensure users dont use both includeInSnippet and clientSideAvailablity * chore: pr feedback - update deprecation message, always read both sdk client values * feat: set both includeInSnippet and clientSideAvailability to computed properties * wip: attempt to use computed and update/create logic blocks * add comment * use getOk instead of getOkExists for nested, rename vars * wip: use getOk instead of getOkExists for nested, test modified read * temp: temporary test changes for local debugging * chore: add some comments * feat: working apart from switching back to deprecated option in edge case * chore: remove commented out code * fix: fix issue with read * test: reset tests * fix: fix typo in read * fix: account for datasources in featureflag read * test: add tests for clientSideAvailability changes * docs: update example docs * docs: add include_in_snippet to website docs and add deprecation notice * docs: update docs * chore: update CHANGELOG * fix: fix most issues caused by go client v7 merge * fix: fix other merge issues * chore: update CHANGELOG * chore: apply codereview keys suggestions Co-authored-by: Isabelle Miller * chore: Update launchdarkly/feature_flags_helper.go keys Co-authored-by: Isabelle Miller * chore: update keys.go to reflect clientsideavailablity keys * chore: go fmt * test: add tests for switching between CSA and IIS, clean up * chore: go fmt * test: remove pure create tests * chore: add TODOs where required * test: add new test case for reverting to default CSA * feat: set clientSideAvailability according to project defaults for flags * test: update tests accordingly and add comments * chore: fmt * refactor project default get into its own function * test: account for default project CSA settings * feat: basic working implementation of resetting to defaults using customizeDiff * feat: attempted changes to customdiff to support CSA * chore: clean up flag resource read * chore: clean up comments and todos * docs: update docs to mention project level defaults for flag sdk settings * chore: remove prints to clean up test logs * chore: go mod vendor * chore: Update CHANGELOG.md Co-authored-by: Isabelle Miller * chore: address pr feedback * docs: update changelog Co-authored-by: Isabelle Miller Co-authored-by: Henry Barrow Co-authored-by: Fabian * 2.2.0 Co-authored-by: Isabelle Miller Co-authored-by: Sunny Co-authored-by: Henry Barrow Co-authored-by: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> Co-authored-by: Isabelle Miller Co-authored-by: Sunny Co-authored-by: Henry Barrow Co-authored-by: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 544944cc..96c2fc46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [Unreleased] +## [2.2.0] (December 23, 2021) ENHANCEMENTS: From 4276c30bb96fbb8479c102ff785cf826d6634f6c Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 31 Dec 2021 12:32:25 +0000 Subject: [PATCH 15/36] [SC-135367] Project resource should support default flag SDK values (#166) * chore: update project resource schema * chore: add new keys to keys.go * feat: most situations working for plan/apply * feat: use customizeDiff to account for resetting default sdk values One slight discrepancy left - switching from using defaultClientSideAvailability to not using the attribute applies the correct changes, but ONLY shows changes to includeInSnippetByDefault in the plan, NOT changes to defaultClientSideAvailability (if applicable) * test: add tests for project changes * docs: update docs to reflect changes * docs: update changelog * chore: account for changes to project dataSource * fix: only set include_in_snippet for non data source projects * chore: make default_csa nested fields required * chore: deprecate data source attr client_side_availability but maintain support * docs: update docs and changelog * docs: update project resource docs according to suggestions Co-authored-by: Henry Barrow * chore: only set datasource deprecated attribute on ds reads * docs: Apply suggestions from code review Co-authored-by: Henry Barrow Co-authored-by: Henry Barrow --- CHANGELOG.md | 12 ++ .../data_source_launchdarkly_project.go | 21 ++- .../data_source_launchdarkly_project_test.go | 3 + launchdarkly/keys.go | 167 +++++++++--------- launchdarkly/project_helper.go | 36 ++-- .../resource_launchdarkly_feature_flag.go | 4 +- launchdarkly/resource_launchdarkly_project.go | 99 ++++++++++- .../resource_launchdarkly_project_test.go | 68 +++++++ website/docs/d/project.html.markdown | 7 +- website/docs/r/project.html.markdown | 13 +- 10 files changed, 318 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c2fc46..8c2a0ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [UNRELEASED] (unreleased) + +FEATURES: + +- Added `default_client_side_availability` block to the `launchdarkly_project` resource to specify whether feature flags created under the project should be available to client-side SDKs by default. + +NOTES: + +- The `launchdarkly_project` resource's argument `include_in_snippet` has been deprecated in favor of `default_client_side_availability`. Please update your config to use `default_client_side_availability` in order to maintain compatibility with future versions. + +- The `launchdarkly_project` data source's attribute `client_side_availability` has been renamed to `default_client_side_availability`. Please update your config to use `default_client_side_availability` in order to maintain compatibility with future versions. + ## [2.2.0] (December 23, 2021) ENHANCEMENTS: diff --git a/launchdarkly/data_source_launchdarkly_project.go b/launchdarkly/data_source_launchdarkly_project.go index dd62b22d..3d410b4b 100644 --- a/launchdarkly/data_source_launchdarkly_project.go +++ b/launchdarkly/data_source_launchdarkly_project.go @@ -18,8 +18,9 @@ func dataSourceProject() *schema.Resource { Computed: true, }, CLIENT_SIDE_AVAILABILITY: { - Type: schema.TypeList, - Computed: true, + Type: schema.TypeList, + Computed: true, + Deprecated: "'client_side_availability' is now deprecated. Please migrate to 'default_client_side_availability' to maintain future compatability.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "using_environment_id": { @@ -33,6 +34,22 @@ func dataSourceProject() *schema.Resource { }, }, }, + DEFAULT_CLIENT_SIDE_AVAILABILITY: { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "using_environment_id": { + Type: schema.TypeBool, + Required: true, + }, + "using_mobile_key": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, TAGS: tagsSchema(), }, } diff --git a/launchdarkly/data_source_launchdarkly_project_test.go b/launchdarkly/data_source_launchdarkly_project_test.go index a83522cc..d6bcbd85 100644 --- a/launchdarkly/data_source_launchdarkly_project_test.go +++ b/launchdarkly/data_source_launchdarkly_project_test.go @@ -106,8 +106,11 @@ func TestAccDataSourceProject_exists(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", project.Name), resource.TestCheckResourceAttr(resourceName, "id", project.Id), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + // TODO: remove deprecated client_side_availability attribute tests pending next major release resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "false"), ), }, }, diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index f7ebc1bc..80c0b8b9 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -3,87 +3,88 @@ package launchdarkly const ( // keys used in terraform files referencing keys in launchdarkly resource objects. // The name of each constant is the same as its value. - PROJECT_KEY = "project_key" - ENV_KEY = "env_key" - KEY = "key" - FLAG_ID = "flag_id" - NAME = "name" - TAGS = "tags" - ENVIRONMENTS = "environments" - API_KEY = "api_key" - MOBILE_KEY = "mobile_key" - CLIENT_SIDE_ID = "client_side_id" - COLOR = "color" - DEFAULT_TTL = "default_ttl" - SECURE_MODE = "secure_mode" - DEFAULT_TRACK_EVENTS = "default_track_events" - REQUIRE_COMMENTS = "require_comments" - CONFIRM_CHANGES = "confirm_changes" - DESCRIPTION = "description" - MAINTAINER_ID = "maintainer_id" - VARIATION_TYPE = "variation_type" - VARIATIONS = "variations" - TEMPORARY = "temporary" - INCLUDE_IN_SNIPPET = "include_in_snippet" - VALUE = "value" - URL = "url" - SECRET = "secret" - ENABLED = "enabled" - ON = "on" - RESOURCES = "resources" - NOT_RESOURCES = "not_resources" - ACTIONS = "actions" - NOT_ACTIONS = "not_actions" - EFFECT = "effect" - POLICY = "policy" - STATEMENTS = "statements" - POLICY_STATEMENTS = "policy_statements" - INLINE_ROLES = "inline_roles" - EXCLUDED = "excluded" - INCLUDED = "included" - CREATION_DATE = "creation_date" - CUSTOM_PROPERTIES = "custom_properties" - EMAIL = "email" - FIRST_NAME = "first_name" - LAST_NAME = "last_name" - ROLE = "role" - CUSTOM_ROLES = "custom_roles" - RULES = "rules" - ATTRIBUTE = "attribute" - OP = "op" - VALUES = "values" - VALUE_TYPE = "value_type" - NEGATE = "negate" - CLAUSES = "clauses" - WEIGHT = "weight" - BUCKET_BY = "bucket_by" - ROLLOUT_WEIGHTS = "rollout_weights" - VARIATION = "variation" - TARGETS = "targets" - PREREQUISITES = "prerequisites" - FLAG_KEY = "flag_key" - TRACK_EVENTS = "track_events" - FALLTHROUGH = "fallthrough" - KIND = "kind" - CONFIG = "config" - DEFAULT_ON_VARIATION = "default_on_variation" - DEFAULT_OFF_VARIATION = "default_off_variation" - DEFAULTS = "defaults" - ON_VARIATION = "on_variation" - OFF_VARIATION = "off_variation" - SERVICE_TOKEN = "service_token" - DEFAULT_API_VERSION = "default_api_version" - TOKEN = "token" - EXPIRE = "expire" - ID = "id" - CLIENT_SIDE_AVAILABILITY = "client_side_availability" - ARCHIVED = "archived" - APPROVAL_SETTINGS = "approval_settings" - REQUIRED = "required" - CAN_REVIEW_OWN_REQUEST = "can_review_own_request" - MIN_NUM_APPROVALS = "min_num_approvals" - CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" - REQUIRED_APPROVAL_TAGS = "required_approval_tags" - USING_ENVIRONMENT_ID = "using_environment_id" - USING_MOBILE_KEY = "using_mobile_key" + PROJECT_KEY = "project_key" + ENV_KEY = "env_key" + KEY = "key" + FLAG_ID = "flag_id" + NAME = "name" + TAGS = "tags" + ENVIRONMENTS = "environments" + API_KEY = "api_key" + MOBILE_KEY = "mobile_key" + CLIENT_SIDE_ID = "client_side_id" + COLOR = "color" + DEFAULT_TTL = "default_ttl" + SECURE_MODE = "secure_mode" + DEFAULT_TRACK_EVENTS = "default_track_events" + REQUIRE_COMMENTS = "require_comments" + CONFIRM_CHANGES = "confirm_changes" + DESCRIPTION = "description" + MAINTAINER_ID = "maintainer_id" + VARIATION_TYPE = "variation_type" + VARIATIONS = "variations" + TEMPORARY = "temporary" + INCLUDE_IN_SNIPPET = "include_in_snippet" + VALUE = "value" + URL = "url" + SECRET = "secret" + ENABLED = "enabled" + ON = "on" + RESOURCES = "resources" + NOT_RESOURCES = "not_resources" + ACTIONS = "actions" + NOT_ACTIONS = "not_actions" + EFFECT = "effect" + POLICY = "policy" + STATEMENTS = "statements" + POLICY_STATEMENTS = "policy_statements" + INLINE_ROLES = "inline_roles" + EXCLUDED = "excluded" + INCLUDED = "included" + CREATION_DATE = "creation_date" + CUSTOM_PROPERTIES = "custom_properties" + EMAIL = "email" + FIRST_NAME = "first_name" + LAST_NAME = "last_name" + ROLE = "role" + CUSTOM_ROLES = "custom_roles" + RULES = "rules" + ATTRIBUTE = "attribute" + OP = "op" + VALUES = "values" + VALUE_TYPE = "value_type" + NEGATE = "negate" + CLAUSES = "clauses" + WEIGHT = "weight" + BUCKET_BY = "bucket_by" + ROLLOUT_WEIGHTS = "rollout_weights" + VARIATION = "variation" + TARGETS = "targets" + PREREQUISITES = "prerequisites" + FLAG_KEY = "flag_key" + TRACK_EVENTS = "track_events" + FALLTHROUGH = "fallthrough" + KIND = "kind" + CONFIG = "config" + DEFAULT_ON_VARIATION = "default_on_variation" + DEFAULT_OFF_VARIATION = "default_off_variation" + DEFAULTS = "defaults" + ON_VARIATION = "on_variation" + OFF_VARIATION = "off_variation" + SERVICE_TOKEN = "service_token" + DEFAULT_API_VERSION = "default_api_version" + TOKEN = "token" + EXPIRE = "expire" + ID = "id" + CLIENT_SIDE_AVAILABILITY = "client_side_availability" + DEFAULT_CLIENT_SIDE_AVAILABILITY = "default_client_side_availability" + ARCHIVED = "archived" + APPROVAL_SETTINGS = "approval_settings" + REQUIRED = "required" + CAN_REVIEW_OWN_REQUEST = "can_review_own_request" + MIN_NUM_APPROVALS = "min_num_approvals" + CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" + REQUIRED_APPROVAL_TAGS = "required_approval_tags" + USING_ENVIRONMENT_ID = "using_environment_id" + USING_MOBILE_KEY = "using_mobile_key" ) diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index 78779578..abcbfb81 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -27,9 +27,18 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er } project := rawProject.(ldapi.Project) - // the Id needs to be set on reads for the data source, but it will mess up the state for resource reads + defaultCSA := *project.DefaultClientSideAvailability + clientSideAvailability := []map[string]interface{}{{ + "using_environment_id": defaultCSA.UsingEnvironmentId, + "using_mobile_key": defaultCSA.UsingMobileKey, + }} + // the Id and deprecated client_side_availability need to be set on reads for the data source, but it will mess up the state for resource reads if isDataSource { d.SetId(project.Id) + err = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) + if err != nil { + return fmt.Errorf("could not set client_side_availability on project with key %q: %v", project.Key, err) + } } _ = d.Set(KEY, project.Key) _ = d.Set(NAME, project.Name) @@ -68,27 +77,22 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er if err != nil { return fmt.Errorf("could not set environments on project with key %q: %v", project.Key, err) } + + err = d.Set(INCLUDE_IN_SNIPPET, project.IncludeInSnippetByDefault) + if err != nil { + return fmt.Errorf("could not set include_in_snippet on project with key %q: %v", project.Key, err) + } } err = d.Set(TAGS, project.Tags) if err != nil { return fmt.Errorf("could not set tags on project with key %q: %v", project.Key, err) } - if isDataSource { - defaultCSA := *project.DefaultClientSideAvailability - clientSideAvailability := []map[string]interface{}{{ - "using_environment_id": defaultCSA.UsingEnvironmentId, - "using_mobile_key": defaultCSA.UsingMobileKey, - }} - err = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) - if err != nil { - return fmt.Errorf("could not set client_side_availability on project with key %q: %v", project.Key, err) - } - } else { - err = d.Set(INCLUDE_IN_SNIPPET, project.IncludeInSnippetByDefault) - if err != nil { - return fmt.Errorf("could not set include_in_snippet on project with key %q: %v", project.Key, err) - } + + err = d.Set(DEFAULT_CLIENT_SIDE_AVAILABILITY, clientSideAvailability) + if err != nil { + return fmt.Errorf("could not set default_client_side_availability on project with key %q: %v", project.Key, err) } + return nil } diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 2420b5fe..be715bac 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -10,7 +10,7 @@ import ( ) // We assign a custom diff in cases where the customer has not assigned CSA or IIS in config for a flag in order to respect project level defaults -func customizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { +func customizeFlagDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { config := diff.GetRawConfig() client := v.(*Client) projectKey := diff.Get(PROJECT_KEY).(string) @@ -70,7 +70,7 @@ func resourceFeatureFlag() *schema.Resource { State: resourceFeatureFlagImport, }, Schema: schemaMap, - CustomizeDiff: customizeDiff, + CustomizeDiff: customizeFlagDiff, } } diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index c2a8bcef..20deb29a 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -10,6 +10,40 @@ import ( ldapi "github.com/launchdarkly/api-client-go/v7" ) +// We assign a custom diff in cases where the customer has not assigned a default for CSA or IIS in config +// in order to respect the LD backend defaults and reflect that in our plans +func customizeProjectDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + config := diff.GetRawConfig() + + // Below values will exist due to the schema, we need to check if they are all null + snippetInConfig := config.GetAttr(INCLUDE_IN_SNIPPET) + csaInConfig := config.GetAttr(DEFAULT_CLIENT_SIDE_AVAILABILITY) + + // If we have no keys in the CSA block in the config (length is 0) we know the customer hasn't set any CSA values + csaKeys := csaInConfig.AsValueSlice() + if len(csaKeys) == 0 { + // When we have no values for either clienSideAvailability or includeInSnippet + // Force an UPDATE call by setting a new value for INCLUDE_IN_SNIPPET in the diff according to project defaults + if snippetInConfig.IsNull() { + // We set our values to the LD backend defaults in order to guarantee an update call happening + // If we don't do this, we can run into an edge case described below + // IF previous value of INCLUDE_IN_SNIPPET was false + // AND the project default value for INCLUDE_IN_SNIPPET is true + // AND the customer removes the INCLUDE_IN_SNIPPET key from the config without replacing with defaultCSA + // The read would assume no changes are needed, HOWEVER we need to jump back to LD set defaults + // Hence the setting below + diff.SetNew(INCLUDE_IN_SNIPPET, false) + diff.SetNew(CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ + USING_ENVIRONMENT_ID: false, + USING_MOBILE_KEY: true, + }}) + + } + + } + + return nil +} func resourceProject() *schema.Resource { return &schema.Resource{ Create: resourceProjectCreate, @@ -18,6 +52,8 @@ func resourceProject() *schema.Resource { Delete: resourceProjectDelete, Exists: resourceProjectExists, + CustomizeDiff: customizeProjectDiff, + Importer: &schema.ResourceImporter{ StateContext: resourceProjectImport, }, @@ -36,10 +72,35 @@ func resourceProject() *schema.Resource { Description: "A human-readable name for your project", }, INCLUDE_IN_SNIPPET: { - Type: schema.TypeBool, - Optional: true, - Description: "Whether feature flags created under the project should be available to client-side SDKs by default", - Default: false, + Type: schema.TypeBool, + Optional: true, + Description: "Whether feature flags created under the project should be available to client-side SDKs by default", + Computed: true, + Deprecated: "'include_in_snippet' is now deprecated. Please migrate to 'default_client_side_availability' to maintain future compatability.", + ConflictsWith: []string{DEFAULT_CLIENT_SIDE_AVAILABILITY}, + }, + DEFAULT_CLIENT_SIDE_AVAILABILITY: { + Type: schema.TypeList, + Optional: true, + // Can't set defaults for lists/sets :( https://github.com/hashicorp/terraform-plugin-sdk/issues/142 + // Since we can't set defaults, we run into misleading plans when users remove this attribute from their config + // As the plan output suggests the values will be changed to -> null, when we actually have LD set defaults of false and true respectively + // Sorting that by using Computed for now + Computed: true, + Description: "List determining which SDKs have access to new flags created under the project by default", + ConflictsWith: []string{INCLUDE_IN_SNIPPET}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + USING_ENVIRONMENT_ID: { + Type: schema.TypeBool, + Required: true, + }, + USING_MOBILE_KEY: { + Type: schema.TypeBool, + Required: true, + }, + }, + }, }, TAGS: tagsSchema(), ENVIRONMENTS: { @@ -95,12 +156,38 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { projectKey := d.Get(KEY).(string) projName := d.Get(NAME) projTags := stringsFromResourceData(d, TAGS) - includeInSnippet := d.Get(INCLUDE_IN_SNIPPET) + includeInSnippet := d.Get(INCLUDE_IN_SNIPPET).(bool) + + snippetHasChange := d.HasChange(INCLUDE_IN_SNIPPET) + clientSideHasChange := d.HasChange(DEFAULT_CLIENT_SIDE_AVAILABILITY) + // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk + // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) + _, clientSideAvailabilityOk := d.GetOk(DEFAULT_CLIENT_SIDE_AVAILABILITY) + defaultClientSideAvailability := &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: d.Get(fmt.Sprintf("%s.0.using_environment_id", DEFAULT_CLIENT_SIDE_AVAILABILITY)).(bool), + UsingMobileKey: d.Get(fmt.Sprintf("%s.0.using_mobile_key", DEFAULT_CLIENT_SIDE_AVAILABILITY)).(bool), + } patch := []ldapi.PatchOperation{ patchReplace("/name", &projName), patchReplace("/tags", &projTags), - patchReplace("/includeInSnippetByDefault", includeInSnippet), + } + + if clientSideAvailabilityOk && clientSideHasChange { + patch = append(patch, patchReplace("/defaultClientSideAvailability", defaultClientSideAvailability)) + } else if includeInSnippetOk && snippetHasChange { + // If includeInSnippet is set, still use clientSideAvailability behind the scenes in order to switch UsingMobileKey to false if needed + patch = append(patch, patchReplace("/defaultClientSideAvailability", &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: includeInSnippet, + UsingMobileKey: true, + })) + } else { + // If the user doesn't set either CSA or IIS in config, we set defaults to match API behaviour + patch = append(patch, patchReplace("/defaultClientSideAvailability", &ldapi.ClientSideAvailabilityPost{ + UsingEnvironmentId: false, + UsingMobileKey: true, + })) } _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index b98ff972..24d3c44d 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -133,6 +133,23 @@ resource "launchdarkly_project" "env_test" { color = "AAAAAA" } } +` + + testAccProjectClientSideAvailabilityTrue = ` +resource "launchdarkly_project" "test" { + key = "%s" + name = "test project" + default_client_side_availability { + using_environment_id = true + using_mobile_key = true + } + tags = [ "terraform", "test" ] + environments { + name = "Test Environment" + key = "test-env" + color = "010101" + } +} ` ) @@ -225,6 +242,57 @@ func TestAccProject_Update(t *testing.T) { }) } +func TestAccProject_CSA_Update_And_Revert(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_project.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccProjectCreate, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), + ), + }, + { + Config: fmt.Sprintf(testAccProjectClientSideAvailabilityTrue, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "true"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), + ), + }, + { // make sure that removal of optional attributes reverts them to their default value + Config: fmt.Sprintf(testAccProjectUpdateRemoveOptional, projectKey), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "key", projectKey), + resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), + resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "false"), + resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccProject_WithEnvironments(t *testing.T) { projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resourceName := "launchdarkly_project.env_test" diff --git a/website/docs/d/project.html.markdown b/website/docs/d/project.html.markdown index ca8cf701..c4160fbe 100644 --- a/website/docs/d/project.html.markdown +++ b/website/docs/d/project.html.markdown @@ -31,13 +31,16 @@ In addition to the arguments above, the resource exports the following attribute - `name` - The project's name. -- `client_side_availability` - A map describing whether flags in this project are available to the client-side JavaScript SDK by default. To learn more, read [Nested Client-Side Availability Block](#nested-client-side-availability-block). +- `client_side_availability` - **Deprecated** A map describing which client-side SDKs can use new flags by default. To learn more, read [Nested Client-Side Availability Block](#nested-client-side-availability-block). +Please migrate to `default_client_side_availability` to maintain future compatability. + +- `default_client_side_availability` - A block describing which client-side SDKs can use new flags by default. To learn more, read [Nested Client-Side Availability Block](#nested-client-side-availability-block). - `tags` - The project's set of tags. ### Nested Client-Side Availibility Block -The nested `client_side_availability` block has the following attributes: +The nested `default_client_side_availability` block has the following attributes: - `using_environment_id` - When set to true, the flags in this project are available to SDKs using the client-side ID by default. diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 9c896d0e..829f894d 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -51,10 +51,13 @@ resource "launchdarkly_project" "example" { - `name` - (Required) The project's name. - `environments` - (Required) List of nested `environments` blocks describing LaunchDarkly environments that belong to the project. When managing LaunchDarkly projects in Terraform, you should always manage your environments as nested project resources. To learn more, read [Nested Environments Blocks](#nested-environments-blocks). +### Nested Environments Blocks -> **Note:** Mixing the use of nested `environments` blocks and [`launchdarkly_environment`](/docs/providers/launchdarkly/r/environment.html) resources is not recommended. `launchdarkly_environment` resources should only be used when the encapsulating project is not managed in Terraform. -- `include_in_snippet - (Optional) Whether feature flags created under the project should be available to client-side SDKs by default. +- `include_in_snippet` - **Deprecated** (Optional) Whether feature flags created under the project should be available to client-side SDKs by default. Please migrate to `default_client_side_availability` to maintain future compatibility. + +- `default_client_side_availability` - (Optional) A block describing which client-side SDKs can use new flags by default. To learn more, read [Nested Client Side Availability Block](#nested-client-side-availability-block). - `tags` - (Optional) The project's set of tags. @@ -96,6 +99,14 @@ Nested environments `approval_settings` blocks have the following structure: - `required_approval_tags` - An array of tags used to specify which flags with those tags require approval. You may only set `required_approval_tags` if `required` is not set to `true` and vice versa. +### Nested Client side Availibility Block + +The nested `default_client_side_availability` block describes which client-side SDKs can use new flags by default. To learn more about this setting, read [Making flags available to client-side and mobile SDKs](https://docs.launchdarkly.com/home/getting-started/feature-flags#making-flags-available-to-client-side-and-mobile-sdks). This block has the following structure: + +- `using_environment_id` - (Required) Whether feature flags created under the project are available to JavaScript SDKs using the client-side ID by default. Defaults to `false` when not using `default_client_side_availability`. + +- `using_mobile_key` - (Required) Whether feature flags created under the project are available to mobile SDKs, and other non-JavaScript SDKs, using a mobile key by default. Defaults to `true` when not using `default_client_side_availability`. + ## Import LaunchDarkly projects can be imported using the project's key, e.g. From b6bf635d28effe9c63a34bd7853b4d6de925b004 Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 5 Jan 2022 12:35:16 +0000 Subject: [PATCH 16/36] Fix nil pointer dereference when reading empty approval settings (#169) * Fix nil approval settings bug and clean up circleci config.yml * Update Changelog --- .circleci/config.yml | 22 +----- CHANGELOG.md | 10 ++- go.mod | 1 - launchdarkly/environments_helper.go | 15 +++- launchdarkly/environments_helper_test.go | 93 ++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 85c7d208..fafed610 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,29 +17,11 @@ jobs: name: go vet command: make vet - run: - name: Test Custom Properties - command: TESTARGS="-run TestCustomProperties" make testacc + name: Run unit tests + command: TESTARGS="-v" make test - run: name: Test Data Sources command: TESTARGS="-run TestAccDataSource" make testacc - - run: - name: Test Default Variations - command: TESTARGS="-run TestDefaultVariations" make testacc - - run: - name: Test Environment Helper - command: TESTARGS="-run TestEnvironmentPost" make testacc - - run: - name: Test Target Helper - command: TESTARGS="-run TestTargets" make testacc - - run: - name: Test Variations Helper - command: TESTARGS="-run TestVariations" make testacc - - run: - name: Test Handlers - command: TESTARGS="-run TestHandle" make testacc - - run: - name: Test Policy Statements - command: TESTARGS="-run TestPolicyStatement" make testacc - run: name: Test Access Token Resource command: TESTARGS="-run TestAccAccessToken" make testacc diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2a0ac2..c9928eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ FEATURES: - Added `default_client_side_availability` block to the `launchdarkly_project` resource to specify whether feature flags created under the project should be available to client-side SDKs by default. -NOTES: +BUG FIXES: + +- Fixed a bug in the `launchdarkly_project` and `launchdarkly_environment` resources which caused Terraform to crash when environment approvals settings are omitted from the LaunchDarkly API response. + +NOTES: - The `launchdarkly_project` resource's argument `include_in_snippet` has been deprecated in favor of `default_client_side_availability`. Please update your config to use `default_client_side_availability` in order to maintain compatibility with future versions. @@ -19,9 +23,9 @@ ENHANCEMENTS: FEATURES: -- Added `client_side_availability` block to the `launchdarkly_feature_flag` resource to allow setting whether this flag should be made available to the client-side JavaScript SDK using the client-side ID, mobile key, or both. +- Added `client_side_availability` block to the `launchdarkly_feature_flag` resource to allow setting whether this flag should be made available to the client-side JavaScript SDK using the client-side ID, mobile key, or both. -NOTES: +NOTES: - The `launchdarkly_feature_flag` resource's argument `include_in_snippet` has been deprecated in favor of `client_side_availability`. Please update your config to use `client_side_availability` in order to maintain compatibility with future versions. diff --git a/go.mod b/go.mod index 2842d7e6..92b4b1c6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/fatih/color v1.13.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.0.0 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect github.com/hashicorp/hcl/v2 v2.11.1 // indirect diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index a7ca3ba5..2703fb4c 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -192,7 +192,7 @@ func environmentsToResourceDataMap(envs []ldapi.Environment) map[string]envResou } func environmentToResourceData(env ldapi.Environment) envResourceData { - return envResourceData{ + envData := envResourceData{ KEY: env.Key, NAME: env.Name, API_KEY: env.ApiKey, @@ -205,8 +205,11 @@ func environmentToResourceData(env ldapi.Environment) envResourceData { REQUIRE_COMMENTS: env.RequireComments, CONFIRM_CHANGES: env.ConfirmChanges, TAGS: env.Tags, - APPROVAL_SETTINGS: approvalSettingsToResourceData(*env.ApprovalSettings), } + if env.ApprovalSettings != nil { + envData[APPROVAL_SETTINGS] = approvalSettingsToResourceData(*env.ApprovalSettings) + } + return envData } func rawEnvironmentConfigsToKeyList(rawEnvs []interface{}) []string { @@ -250,7 +253,13 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool _ = d.Set(TAGS, env.Tags) _ = d.Set(REQUIRE_COMMENTS, env.RequireComments) _ = d.Set(CONFIRM_CHANGES, env.ConfirmChanges) - _ = d.Set(APPROVAL_SETTINGS, approvalSettingsToResourceData(*env.ApprovalSettings)) + + if env.ApprovalSettings != nil { + err = d.Set(APPROVAL_SETTINGS, approvalSettingsToResourceData(*env.ApprovalSettings)) + if err != nil { + return err + } + } return nil } diff --git a/launchdarkly/environments_helper_test.go b/launchdarkly/environments_helper_test.go index b877d7b1..6ed588a6 100644 --- a/launchdarkly/environments_helper_test.go +++ b/launchdarkly/environments_helper_test.go @@ -4,6 +4,7 @@ import ( "testing" ldapi "github.com/launchdarkly/api-client-go/v7" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -49,5 +50,97 @@ func TestEnvironmentPostFromResourceData(t *testing.T) { require.Equal(t, tc.expected, actual) }) } +} +func TestEnvironmentToResourceData(t *testing.T) { + testCases := []struct { + name string + input ldapi.Environment + expected envResourceData + }{ + { + name: "standard environment", + input: ldapi.Environment{ + Key: "test-env", + Name: "Test Env", + ApiKey: "sdk-234123123", + MobileKey: "b1235363456", + Id: "b234234234234", + Color: "FFFFFF", + DefaultTtl: 60, + SecureMode: true, + DefaultTrackEvents: true, + RequireComments: true, + ConfirmChanges: true, + Tags: []string{"test"}, + ApprovalSettings: &ldapi.ApprovalSettings{ + Required: true, + MinNumApprovals: 3, + CanApplyDeclinedChanges: true, + RequiredApprovalTags: []string{"approval"}, + CanReviewOwnRequest: true, + }, + }, + expected: envResourceData{ + KEY: "test-env", + NAME: "Test Env", + API_KEY: "sdk-234123123", + MOBILE_KEY: "b1235363456", + CLIENT_SIDE_ID: "b234234234234", + COLOR: "FFFFFF", + DEFAULT_TTL: 60, + SECURE_MODE: true, + DEFAULT_TRACK_EVENTS: true, + REQUIRE_COMMENTS: true, + CONFIRM_CHANGES: true, + TAGS: []string{"test"}, + APPROVAL_SETTINGS: []map[string]interface{}{ + { + CAN_REVIEW_OWN_REQUEST: true, + MIN_NUM_APPROVALS: int32(3), + CAN_APPLY_DECLINED_CHANGES: true, + REQUIRED_APPROVAL_TAGS: []string{"approval"}, + REQUIRED: true, + }, + }, + }, + }, + { + name: "without approval settings", + input: ldapi.Environment{ + Key: "test-env", + Name: "Test Env", + ApiKey: "sdk-234123123", + MobileKey: "b1235363456", + Id: "b234234234234", + Color: "FFFFFF", + DefaultTtl: 60, + SecureMode: true, + DefaultTrackEvents: true, + RequireComments: true, + ConfirmChanges: true, + Tags: []string{"test"}, + }, + expected: envResourceData{ + KEY: "test-env", + NAME: "Test Env", + API_KEY: "sdk-234123123", + MOBILE_KEY: "b1235363456", + CLIENT_SIDE_ID: "b234234234234", + COLOR: "FFFFFF", + DEFAULT_TTL: 60, + SECURE_MODE: true, + DEFAULT_TRACK_EVENTS: true, + REQUIRE_COMMENTS: true, + CONFIRM_CHANGES: true, + TAGS: []string{"test"}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := environmentToResourceData(tc.input) + assert.Equal(t, tc.expected, actual) + }) + } } From 0fab9f1ee2b1857e4674d13e5c5ecb1265386c25 Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 5 Jan 2022 13:47:51 +0000 Subject: [PATCH 17/36] Backmerge v2.3.0 (#170) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9928eb8..edf56fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [UNRELEASED] (unreleased) +## [2.3.0] (January 4, 2022) FEATURES: From b21eb91385b00a12a5fc971c87d9a12cef8743eb Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Thu, 6 Jan 2022 16:45:24 +0100 Subject: [PATCH 18/36] Imiller/sc 136707/streamline tests to use key constants where (#172) * update data source tests * add a pre-commit to alphabetize the keys * resources * changelog --- .pre-commit-config.yaml | 5 + CHANGELOG.md | 8 + ...ta_source_launchdarkly_environment_test.go | 18 +-- ...nchdarkly_feature_flag_environment_test.go | 18 +-- ...a_source_launchdarkly_feature_flag_test.go | 17 ++- .../data_source_launchdarkly_project_test.go | 10 +- .../data_source_launchdarkly_segment_test.go | 14 +- ...ta_source_launchdarkly_team_member_test.go | 10 +- .../data_source_launchdarkly_webhook_test.go | 12 +- launchdarkly/keys.go | 143 +++++++++--------- ...resource_launchdarkly_access_token_test.go | 68 ++++----- .../resource_launchdarkly_custom_role_test.go | 24 +-- .../resource_launchdarkly_destination_test.go | 102 ++++++------- .../resource_launchdarkly_environment_test.go | 126 +++++++-------- ...nchdarkly_feature_flag_environment_test.go | 52 +++---- ...resource_launchdarkly_feature_flag_test.go | 134 ++++++++-------- .../resource_launchdarkly_project_test.go | 56 +++---- .../resource_launchdarkly_segment_test.go | 54 +++---- .../resource_launchdarkly_team_member_test.go | 42 ++--- .../resource_launchdarkly_webhook_test.go | 60 ++++---- 20 files changed, 494 insertions(+), 479 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..85914761 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: git@github.com:ashanbrown/gofmts + rev: v0.1.4 + hooks: + - id: gofmts diff --git a/CHANGELOG.md b/CHANGELOG.md index edf56fbe..d300693a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [2.4.0] (Unreleased) + +ENHANCEMENTS: + +- Updated tests to use the constant attribute keys defined in launchdarkly/keys.go + +- Added a pre-commit file with a hook to alphabetize launchdarkly/keys.go + ## [2.3.0] (January 4, 2022) FEATURES: diff --git a/launchdarkly/data_source_launchdarkly_environment_test.go b/launchdarkly/data_source_launchdarkly_environment_test.go index ece7017f..3b36dc6d 100644 --- a/launchdarkly/data_source_launchdarkly_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_environment_test.go @@ -118,16 +118,16 @@ func TestAccDataSourceEnv_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceEnvironment, envKey, projectKey), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "key"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "color"), - resource.TestCheckResourceAttr(resourceName, "key", env.Key), - resource.TestCheckResourceAttr(resourceName, "name", env.Name), - resource.TestCheckResourceAttr(resourceName, "color", env.Color), + resource.TestCheckResourceAttrSet(resourceName, KEY), + resource.TestCheckResourceAttrSet(resourceName, NAME), + resource.TestCheckResourceAttrSet(resourceName, COLOR), + resource.TestCheckResourceAttr(resourceName, KEY, env.Key), + resource.TestCheckResourceAttr(resourceName, NAME, env.Name), + resource.TestCheckResourceAttr(resourceName, COLOR, env.Color), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, "mobile_key", env.MobileKey), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "0"), - resource.TestCheckResourceAttr(resourceName, "id", projectKey+"/"+env.Key), + resource.TestCheckResourceAttr(resourceName, MOBILE_KEY, env.MobileKey), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "0"), + resource.TestCheckResourceAttr(resourceName, ID, projectKey+"/"+env.Key), ), }, }, diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go index b5ce2f78..400e8e61 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go @@ -161,17 +161,17 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceFeatureFlagEnvironment, envKey, flagId), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "flag_id"), - resource.TestCheckResourceAttr(resourceName, "env_key", envKey), - resource.TestCheckResourceAttr(resourceName, "on", fmt.Sprint(thisConfig.On)), - resource.TestCheckResourceAttr(resourceName, "track_events", fmt.Sprint(thisConfig.TrackEvents)), + resource.TestCheckResourceAttrSet(resourceName, FLAG_ID), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, envKey), + resource.TestCheckResourceAttr(resourceName, ON, fmt.Sprint(thisConfig.On)), + resource.TestCheckResourceAttr(resourceName, TRACK_EVENTS, fmt.Sprint(thisConfig.TrackEvents)), resource.TestCheckResourceAttr(resourceName, "rules.0.variation", fmt.Sprint(*thisConfig.Rules[0].Variation)), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", thisConfig.Rules[0].Clauses[0].Attribute), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.op", thisConfig.Rules[0].Clauses[0].Op), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", fmt.Sprint(thisConfig.Rules[0].Clauses[0].Values[0])), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.flag_key", thisConfig.Prerequisites[0].Key), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.variation", fmt.Sprint(thisConfig.Prerequisites[0].Variation)), - resource.TestCheckResourceAttr(resourceName, "off_variation", fmt.Sprint(*thisConfig.OffVariation)), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, fmt.Sprint(*thisConfig.OffVariation)), resource.TestCheckResourceAttr(resourceName, "targets.0.values.#", fmt.Sprint(len(thisConfig.Targets[0].Values))), resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "1"), ), @@ -179,10 +179,10 @@ func TestAccDataSourceFeatureFlagEnvironment_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceFeatureFlagEnvironment, "production", flagId), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "flag_id"), - resource.TestCheckResourceAttr(resourceName, "env_key", "production"), - resource.TestCheckResourceAttr(resourceName, "on", fmt.Sprint(otherConfig.On)), - resource.TestCheckResourceAttr(resourceName, "track_events", fmt.Sprint(otherConfig.TrackEvents)), + resource.TestCheckResourceAttrSet(resourceName, FLAG_ID), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "production"), + resource.TestCheckResourceAttr(resourceName, ON, fmt.Sprint(otherConfig.On)), + resource.TestCheckResourceAttr(resourceName, TRACK_EVENTS, fmt.Sprint(otherConfig.TrackEvents)), resource.TestCheckResourceAttr(resourceName, "rules.#", fmt.Sprint(len(otherConfig.Rules))), resource.TestCheckResourceAttr(resourceName, "prerequisites.#", fmt.Sprint(len(otherConfig.Prerequisites))), resource.TestCheckResourceAttr(resourceName, "targets.#", fmt.Sprint(len(otherConfig.Targets))), diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_test.go index 5371d023..74b0984c 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_test.go @@ -100,17 +100,18 @@ func TestAccDataSourceFeatureFlag_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceFeatureFlag, flagKey, projectKey), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "key"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttrSet(resourceName, "project_key"), - resource.TestCheckResourceAttr(resourceName, "key", flag.Key), - resource.TestCheckResourceAttr(resourceName, "name", flag.Name), - resource.TestCheckResourceAttr(resourceName, "description", *flag.Description), - resource.TestCheckResourceAttr(resourceName, "temporary", "true"), + resource.TestCheckResourceAttrSet(resourceName, KEY), + resource.TestCheckResourceAttrSet(resourceName, NAME), + resource.TestCheckResourceAttrSet(resourceName, PROJECT_KEY), + resource.TestCheckResourceAttr(resourceName, KEY, flag.Key), + resource.TestCheckResourceAttr(resourceName, NAME, flag.Name), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, *flag.Description), + resource.TestCheckResourceAttr(resourceName, TEMPORARY, "true"), resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckResourceAttr(resourceName, "id", projectKey+"/"+flag.Key), + resource.TestCheckResourceAttr(resourceName, ID, projectKey+"/"+flag.Key), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), ), diff --git a/launchdarkly/data_source_launchdarkly_project_test.go b/launchdarkly/data_source_launchdarkly_project_test.go index d6bcbd85..3eb7b091 100644 --- a/launchdarkly/data_source_launchdarkly_project_test.go +++ b/launchdarkly/data_source_launchdarkly_project_test.go @@ -100,11 +100,11 @@ func TestAccDataSourceProject_exists(t *testing.T) { { Config: fmt.Sprintf(testAccProjectExists, projectKey), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "key"), - resource.TestCheckResourceAttrSet(resourceName, "name"), - resource.TestCheckResourceAttr(resourceName, "key", project.Key), - resource.TestCheckResourceAttr(resourceName, "name", project.Name), - resource.TestCheckResourceAttr(resourceName, "id", project.Id), + resource.TestCheckResourceAttrSet(resourceName, KEY), + resource.TestCheckResourceAttrSet(resourceName, NAME), + resource.TestCheckResourceAttr(resourceName, KEY, project.Key), + resource.TestCheckResourceAttr(resourceName, NAME, project.Name), + resource.TestCheckResourceAttr(resourceName, ID, project.Id), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), // TODO: remove deprecated client_side_availability attribute tests pending next major release resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), diff --git a/launchdarkly/data_source_launchdarkly_segment_test.go b/launchdarkly/data_source_launchdarkly_segment_test.go index 37b4db8c..9562b0d8 100644 --- a/launchdarkly/data_source_launchdarkly_segment_test.go +++ b/launchdarkly/data_source_launchdarkly_segment_test.go @@ -151,12 +151,12 @@ func TestAccDataSourceSegment_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceSegment, segmentKey, projectKey), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "key"), - resource.TestCheckResourceAttr(resourceName, "name", segment.Name), - resource.TestCheckResourceAttr(resourceName, "key", segment.Key), - resource.TestCheckResourceAttr(resourceName, "id", projectKey+"/test/"+segmentKey), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), + resource.TestCheckResourceAttrSet(resourceName, KEY), + resource.TestCheckResourceAttr(resourceName, NAME, segment.Name), + resource.TestCheckResourceAttr(resourceName, KEY, segment.Key), + resource.TestCheckResourceAttr(resourceName, ID, projectKey+"/test/"+segmentKey), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", "name"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.op", "startsWith"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.#", "1"), @@ -165,7 +165,7 @@ func TestAccDataSourceSegment_exists(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "excluded.#", "1"), resource.TestCheckResourceAttr(resourceName, "excluded.0", "some_bad@email.com"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), - resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, CREATION_DATE), ), }, }, diff --git a/launchdarkly/data_source_launchdarkly_team_member_test.go b/launchdarkly/data_source_launchdarkly_team_member_test.go index 0cd29d03..6e653f49 100644 --- a/launchdarkly/data_source_launchdarkly_team_member_test.go +++ b/launchdarkly/data_source_launchdarkly_team_member_test.go @@ -87,11 +87,11 @@ func TestAccDataSourceTeamMember_exists(t *testing.T) { { Config: testAccDataSourceTeamMemberConfig(testMember.Email), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "email"), - resource.TestCheckResourceAttr(resourceName, "email", testMember.Email), - resource.TestCheckResourceAttr(resourceName, "first_name", *testMember.FirstName), - resource.TestCheckResourceAttr(resourceName, "last_name", *testMember.LastName), - resource.TestCheckResourceAttr(resourceName, "id", testMember.Id), + resource.TestCheckResourceAttrSet(resourceName, EMAIL), + resource.TestCheckResourceAttr(resourceName, EMAIL, testMember.Email), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, *testMember.FirstName), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, *testMember.LastName), + resource.TestCheckResourceAttr(resourceName, ID, testMember.Id), ), }, }, diff --git a/launchdarkly/data_source_launchdarkly_webhook_test.go b/launchdarkly/data_source_launchdarkly_webhook_test.go index 5d177c0f..bbfa630d 100644 --- a/launchdarkly/data_source_launchdarkly_webhook_test.go +++ b/launchdarkly/data_source_launchdarkly_webhook_test.go @@ -107,17 +107,17 @@ func TestAccDataSourceWebhook_exists(t *testing.T) { { Config: fmt.Sprintf(testAccDataSourceWebhook, webhook.Id), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttr(resourceName, "id", webhook.Id), - resource.TestCheckResourceAttr(resourceName, "name", webhookName), - resource.TestCheckResourceAttr(resourceName, "url", webhook.Url), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, ID, webhook.Id), + resource.TestCheckResourceAttr(resourceName, NAME, webhookName), + resource.TestCheckResourceAttr(resourceName, URL, webhook.Url), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "turnFlagOn"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), - resource.TestCheckResourceAttr(resourceName, "secret", ""), // since we set Sign to false + resource.TestCheckResourceAttr(resourceName, SECRET, ""), // since we set Sign to false ), }, diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 80c0b8b9..564918db 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -1,90 +1,91 @@ package launchdarkly +// keys used in terraform files referencing keys in launchdarkly resource objects. +// The name of each constant is the same as its value. const ( - // keys used in terraform files referencing keys in launchdarkly resource objects. - // The name of each constant is the same as its value. - PROJECT_KEY = "project_key" - ENV_KEY = "env_key" - KEY = "key" - FLAG_ID = "flag_id" - NAME = "name" - TAGS = "tags" - ENVIRONMENTS = "environments" + //gofmts:sort + ACTIONS = "actions" API_KEY = "api_key" - MOBILE_KEY = "mobile_key" + APPROVAL_SETTINGS = "approval_settings" + ARCHIVED = "archived" + ATTRIBUTE = "attribute" + BUCKET_BY = "bucket_by" + CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" + CAN_REVIEW_OWN_REQUEST = "can_review_own_request" + CLAUSES = "clauses" + CLIENT_SIDE_AVAILABILITY = "client_side_availability" CLIENT_SIDE_ID = "client_side_id" COLOR = "color" - DEFAULT_TTL = "default_ttl" - SECURE_MODE = "secure_mode" - DEFAULT_TRACK_EVENTS = "default_track_events" - REQUIRE_COMMENTS = "require_comments" + CONFIG = "config" CONFIRM_CHANGES = "confirm_changes" - DESCRIPTION = "description" - MAINTAINER_ID = "maintainer_id" - VARIATION_TYPE = "variation_type" - VARIATIONS = "variations" - TEMPORARY = "temporary" - INCLUDE_IN_SNIPPET = "include_in_snippet" - VALUE = "value" - URL = "url" - SECRET = "secret" - ENABLED = "enabled" - ON = "on" - RESOURCES = "resources" - NOT_RESOURCES = "not_resources" - ACTIONS = "actions" - NOT_ACTIONS = "not_actions" - EFFECT = "effect" - POLICY = "policy" - STATEMENTS = "statements" - POLICY_STATEMENTS = "policy_statements" - INLINE_ROLES = "inline_roles" - EXCLUDED = "excluded" - INCLUDED = "included" CREATION_DATE = "creation_date" CUSTOM_PROPERTIES = "custom_properties" - EMAIL = "email" - FIRST_NAME = "first_name" - LAST_NAME = "last_name" - ROLE = "role" CUSTOM_ROLES = "custom_roles" - RULES = "rules" - ATTRIBUTE = "attribute" - OP = "op" - VALUES = "values" - VALUE_TYPE = "value_type" - NEGATE = "negate" - CLAUSES = "clauses" - WEIGHT = "weight" - BUCKET_BY = "bucket_by" - ROLLOUT_WEIGHTS = "rollout_weights" - VARIATION = "variation" - TARGETS = "targets" - PREREQUISITES = "prerequisites" - FLAG_KEY = "flag_key" - TRACK_EVENTS = "track_events" - FALLTHROUGH = "fallthrough" - KIND = "kind" - CONFIG = "config" - DEFAULT_ON_VARIATION = "default_on_variation" - DEFAULT_OFF_VARIATION = "default_off_variation" DEFAULTS = "defaults" - ON_VARIATION = "on_variation" - OFF_VARIATION = "off_variation" - SERVICE_TOKEN = "service_token" DEFAULT_API_VERSION = "default_api_version" - TOKEN = "token" + DEFAULT_CLIENT_SIDE_AVAILABILITY = "default_client_side_availability" + DEFAULT_OFF_VARIATION = "default_off_variation" + DEFAULT_ON_VARIATION = "default_on_variation" + DEFAULT_TRACK_EVENTS = "default_track_events" + DEFAULT_TTL = "default_ttl" + DESCRIPTION = "description" + EFFECT = "effect" + EMAIL = "email" + ENABLED = "enabled" + ENVIRONMENTS = "environments" + ENV_KEY = "env_key" + EXCLUDED = "excluded" EXPIRE = "expire" + FALLTHROUGH = "fallthrough" + FIRST_NAME = "first_name" + FLAG_ID = "flag_id" + FLAG_KEY = "flag_key" ID = "id" - CLIENT_SIDE_AVAILABILITY = "client_side_availability" - DEFAULT_CLIENT_SIDE_AVAILABILITY = "default_client_side_availability" - ARCHIVED = "archived" - APPROVAL_SETTINGS = "approval_settings" - REQUIRED = "required" - CAN_REVIEW_OWN_REQUEST = "can_review_own_request" + INCLUDED = "included" + INCLUDE_IN_SNIPPET = "include_in_snippet" + INLINE_ROLES = "inline_roles" + KEY = "key" + KIND = "kind" + LAST_NAME = "last_name" + MAINTAINER_ID = "maintainer_id" MIN_NUM_APPROVALS = "min_num_approvals" - CAN_APPLY_DECLINED_CHANGES = "can_apply_declined_changes" + MOBILE_KEY = "mobile_key" + NAME = "name" + NEGATE = "negate" + NOT_ACTIONS = "not_actions" + NOT_RESOURCES = "not_resources" + OFF_VARIATION = "off_variation" + ON = "on" + ON_VARIATION = "on_variation" + OP = "op" + POLICY = "policy" + POLICY_STATEMENTS = "policy_statements" + PREREQUISITES = "prerequisites" + PROJECT_KEY = "project_key" + REQUIRED = "required" REQUIRED_APPROVAL_TAGS = "required_approval_tags" + REQUIRE_COMMENTS = "require_comments" + RESOURCES = "resources" + ROLE = "role" + ROLLOUT_WEIGHTS = "rollout_weights" + RULES = "rules" + SECRET = "secret" + SECURE_MODE = "secure_mode" + SERVICE_TOKEN = "service_token" + STATEMENTS = "statements" + TAGS = "tags" + TARGETS = "targets" + TEMPORARY = "temporary" + TOKEN = "token" + TRACK_EVENTS = "track_events" + URL = "url" USING_ENVIRONMENT_ID = "using_environment_id" USING_MOBILE_KEY = "using_mobile_key" + VALUE = "value" + VALUES = "values" + VALUE_TYPE = "value_type" + VARIATION = "variation" + VARIATIONS = "variations" + VARIATION_TYPE = "variation_type" + WEIGHT = "weight" ) diff --git a/launchdarkly/resource_launchdarkly_access_token_test.go b/launchdarkly/resource_launchdarkly_access_token_test.go index cf14544d..3b2cd376 100644 --- a/launchdarkly/resource_launchdarkly_access_token_test.go +++ b/launchdarkly/resource_launchdarkly_access_token_test.go @@ -123,13 +123,13 @@ func TestAccAccessToken_Create(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenCreate, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Access token - "+name), - resource.TestCheckResourceAttr(resourceName, "role", "reader"), - resource.TestCheckResourceAttr(resourceName, "service_token", "false"), - resource.TestCheckResourceAttrSet(resourceName, "default_api_version"), - resource.TestCheckResourceAttrSet(resourceName, "token"), - resource.TestCheckNoResourceAttr(resourceName, "policy"), - resource.TestCheckNoResourceAttr(resourceName, "custom_roles"), + resource.TestCheckResourceAttr(resourceName, NAME, "Access token - "+name), + resource.TestCheckResourceAttr(resourceName, ROLE, "reader"), + resource.TestCheckResourceAttr(resourceName, SERVICE_TOKEN, "false"), + resource.TestCheckResourceAttrSet(resourceName, DEFAULT_API_VERSION), + resource.TestCheckResourceAttrSet(resourceName, TOKEN), + resource.TestCheckNoResourceAttr(resourceName, POLICY), + resource.TestCheckNoResourceAttr(resourceName, CUSTOM_ROLES), ), }, }, @@ -149,13 +149,13 @@ func TestAccAccessToken_CreateWithCustomRole(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenCreateWithCustomRole, name, name, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Access token - "+name), + resource.TestCheckResourceAttr(resourceName, NAME, "Access token - "+name), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), - resource.TestCheckResourceAttr(resourceName, "service_token", "false"), - resource.TestCheckResourceAttrSet(resourceName, "default_api_version"), - resource.TestCheckResourceAttrSet(resourceName, "token"), - resource.TestCheckNoResourceAttr(resourceName, "policy"), - resource.TestCheckNoResourceAttr(resourceName, "role"), + resource.TestCheckResourceAttr(resourceName, SERVICE_TOKEN, "false"), + resource.TestCheckResourceAttrSet(resourceName, DEFAULT_API_VERSION), + resource.TestCheckResourceAttrSet(resourceName, TOKEN), + resource.TestCheckNoResourceAttr(resourceName, POLICY), + resource.TestCheckNoResourceAttr(resourceName, ROLE), ), }, }, @@ -175,13 +175,13 @@ func TestAccAccessToken_CreateWithImmutableParams(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenCreateWithImmutableParams, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Access token - "+name), - resource.TestCheckResourceAttr(resourceName, "role", "reader"), - resource.TestCheckResourceAttr(resourceName, "service_token", "true"), - resource.TestCheckResourceAttr(resourceName, "default_api_version", "20160426"), - resource.TestCheckResourceAttrSet(resourceName, "token"), - resource.TestCheckNoResourceAttr(resourceName, "policy"), - resource.TestCheckNoResourceAttr(resourceName, "custom_roles"), + resource.TestCheckResourceAttr(resourceName, NAME, "Access token - "+name), + resource.TestCheckResourceAttr(resourceName, ROLE, "reader"), + resource.TestCheckResourceAttr(resourceName, SERVICE_TOKEN, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_API_VERSION, "20160426"), + resource.TestCheckResourceAttrSet(resourceName, TOKEN), + resource.TestCheckNoResourceAttr(resourceName, POLICY), + resource.TestCheckNoResourceAttr(resourceName, CUSTOM_ROLES), ), }, }, @@ -201,18 +201,18 @@ func TestAccAccessToken_CreateWithInlineRoles(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenCreateWithInlineRoles, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Access token - "+name), + resource.TestCheckResourceAttr(resourceName, NAME, "Access token - "+name), resource.TestCheckResourceAttr(resourceName, "inline_roles.#", "1"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.actions.#", "1"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.actions.0", "*"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.resources.#", "1"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.resources.0", "proj/*:env/staging"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.effect", "allow"), - resource.TestCheckResourceAttr(resourceName, "service_token", "false"), - resource.TestCheckResourceAttrSet(resourceName, "default_api_version"), - resource.TestCheckResourceAttrSet(resourceName, "token"), - resource.TestCheckNoResourceAttr(resourceName, "role"), - resource.TestCheckNoResourceAttr(resourceName, "custom_roles"), + resource.TestCheckResourceAttr(resourceName, SERVICE_TOKEN, "false"), + resource.TestCheckResourceAttrSet(resourceName, DEFAULT_API_VERSION), + resource.TestCheckResourceAttrSet(resourceName, TOKEN), + resource.TestCheckNoResourceAttr(resourceName, ROLE), + resource.TestCheckNoResourceAttr(resourceName, CUSTOM_ROLES), ), }, }, @@ -232,18 +232,18 @@ func TestAccAccessToken_CreateWithPolicyStatements(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenCreateWithPolicyStatements, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Access token - "+name), + resource.TestCheckResourceAttr(resourceName, NAME, "Access token - "+name), resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.0", "*"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.resources.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.resources.0", "proj/*:env/staging"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.effect", "allow"), - resource.TestCheckResourceAttr(resourceName, "service_token", "false"), - resource.TestCheckResourceAttrSet(resourceName, "default_api_version"), - resource.TestCheckResourceAttrSet(resourceName, "token"), - resource.TestCheckNoResourceAttr(resourceName, "role"), - resource.TestCheckNoResourceAttr(resourceName, "custom_roles"), + resource.TestCheckResourceAttr(resourceName, SERVICE_TOKEN, "false"), + resource.TestCheckResourceAttrSet(resourceName, DEFAULT_API_VERSION), + resource.TestCheckResourceAttrSet(resourceName, TOKEN), + resource.TestCheckNoResourceAttr(resourceName, ROLE), + resource.TestCheckNoResourceAttr(resourceName, CUSTOM_ROLES), ), }, }, @@ -269,7 +269,7 @@ func TestAccAccessToken_Update(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenUpdate, name), // update regular role to policy_statements roles Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Updated - "+name), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated - "+name), resource.TestCheckResourceAttr(resourceName, "inline_roles.#", "1"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.actions.#", "1"), resource.TestCheckResourceAttr(resourceName, "inline_roles.0.actions.0", "*"), @@ -303,7 +303,7 @@ func TestAccAccessToken_UpdateCustomRole(t *testing.T) { Config: fmt.Sprintf(testAccAccessTokenUpdateCustomRole, name, name, name, name, name), Check: resource.ComposeTestCheckFunc( testAccCheckAccessTokenExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Updated - "+name), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated - "+name), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "2"), resource.TestCheckResourceAttr(resourceName, "custom_roles.0", name), resource.TestCheckResourceAttr(resourceName, "custom_roles.1", name+"2"), diff --git a/launchdarkly/resource_launchdarkly_custom_role_test.go b/launchdarkly/resource_launchdarkly_custom_role_test.go index 06e1ec94..afc6bd78 100644 --- a/launchdarkly/resource_launchdarkly_custom_role_test.go +++ b/launchdarkly/resource_launchdarkly_custom_role_test.go @@ -73,9 +73,9 @@ func TestAccCustomRole_Create(t *testing.T) { Config: fmt.Sprintf(testAccCustomRoleCreate, key, name), Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", key), - resource.TestCheckResourceAttr(resourceName, "name", "Custom role - "+name), - resource.TestCheckResourceAttr(resourceName, "description", "Deny all actions on production environments"), + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Custom role - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Deny all actions on production environments"), resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), @@ -102,9 +102,9 @@ func TestAccCustomRole_CreateWithStatements(t *testing.T) { Config: fmt.Sprintf(testAccCustomRoleCreateWithStatements, key, name), Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", key), - resource.TestCheckResourceAttr(resourceName, "name", "Custom role - "+name), - resource.TestCheckResourceAttr(resourceName, "description", "Allow all actions on staging environments"), + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Custom role - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Allow all actions on staging environments"), resource.TestCheckResourceAttr(resourceName, "policy.#", "0"), resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.#", "1"), @@ -143,9 +143,9 @@ func TestAccCustomRole_Update(t *testing.T) { Config: fmt.Sprintf(testAccCustomRoleUpdate, key, name), Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", key), - resource.TestCheckResourceAttr(resourceName, "name", "Updated - "+name), - resource.TestCheckResourceAttr(resourceName, "description", ""), // should be empty after removal + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, ""), // should be empty after removal resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), @@ -178,9 +178,9 @@ func TestAccCustomRole_UpdateWithStatements(t *testing.T) { Config: fmt.Sprintf(testAccCustomRoleUpdateWithStatements, key, name), Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", key), - resource.TestCheckResourceAttr(resourceName, "name", "Updated role - "+name), - resource.TestCheckResourceAttr(resourceName, "description", "Deny all actions on production environments"), + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated role - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Deny all actions on production environments"), resource.TestCheckResourceAttr(resourceName, "policy.#", "0"), resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "policy_statements.0.actions.#", "1"), diff --git a/launchdarkly/resource_launchdarkly_destination_test.go b/launchdarkly/resource_launchdarkly_destination_test.go index 0a9a45c3..5014f519 100644 --- a/launchdarkly/resource_launchdarkly_destination_test.go +++ b/launchdarkly/resource_launchdarkly_destination_test.go @@ -181,9 +181,9 @@ func TestAccDestination_CreateKinesis(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "kinesis-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "kinesis-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.region", "us-east-1"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), @@ -212,9 +212,9 @@ func TestAccDestination_CreateMparticle(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "mparticle-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "mparticle-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.api_key", "apiKeyfromMParticle"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -237,10 +237,10 @@ func TestAccDestination_CreatePubsub(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "pubsub-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "pubsub-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "google-pubsub"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "config.project", "test-project"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -263,9 +263,9 @@ func TestAccDestination_CreateSegment(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "segment"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "segment"), resource.TestCheckResourceAttr(resourceName, "config.write_key", "super-secret-write-key"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -288,9 +288,9 @@ func TestAccDestination_CreateAzureEventHubs(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "azure-event-hubs-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "azure-event-hubs"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "azure-event-hubs-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "azure-event-hubs"), resource.TestCheckResourceAttr(resourceName, "config.namespace", "namespace"), resource.TestCheckResourceAttr(resourceName, "config.name", "name"), resource.TestCheckResourceAttr(resourceName, "config.policy_name", "policy-name"), @@ -316,9 +316,9 @@ func TestAccDestination_UpdateKinesis(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "kinesis-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "kinesis-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.role_arn", "arn:aws:iam::123456789012:role/marketingadmin"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -327,9 +327,9 @@ func TestAccDestination_UpdateKinesis(t *testing.T) { Config: withRandomProject(projectKey, testAccDestinationUpdateKinesis), Check: resource.ComposeTestCheckFunc( testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "updated-kinesis-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "kinesis"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "updated-kinesis-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "kinesis"), resource.TestCheckResourceAttr(resourceName, "config.role_arn", "arn:aws:iam::123456789012:role/marketingadmin"), resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), @@ -353,9 +353,9 @@ func TestAccDestination_UpdatePubsub(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "pubsub-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "pubsub-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "google-pubsub"), resource.TestCheckResourceAttr(resourceName, "config.project", "test-project"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -364,9 +364,9 @@ func TestAccDestination_UpdatePubsub(t *testing.T) { Config: withRandomProject(projectKey, testAccDestinationUpdatePubsub), Check: resource.ComposeTestCheckFunc( testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "updated-pubsub-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "google-pubsub"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "updated-pubsub-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "google-pubsub"), resource.TestCheckResourceAttr(resourceName, "config.project", "renamed-project"), resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), @@ -390,9 +390,9 @@ func TestAccDestination_UpdateMparticle(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "mparticle-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "mparticle-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.secret", "mParticleSecret"), resource.TestCheckResourceAttr(resourceName, "config.environment", "production"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), @@ -402,9 +402,9 @@ func TestAccDestination_UpdateMparticle(t *testing.T) { Config: withRandomProject(projectKey, testAccDestinationUpdateMparticle), Check: resource.ComposeTestCheckFunc( testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "updated-mparticle-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "mparticle"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "updated-mparticle-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "mparticle"), resource.TestCheckResourceAttr(resourceName, "config.secret", "updatedSecret"), resource.TestCheckResourceAttr(resourceName, "config.environment", "production"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), @@ -429,10 +429,10 @@ func TestAccDestination_UpdateSegment(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "segment"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "segment"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "config.write_key", "super-secret-write-key"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -442,10 +442,10 @@ func TestAccDestination_UpdateSegment(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "segment"), - resource.TestCheckResourceAttr(resourceName, "on", "false"), // should default to false when removed + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "segment"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), // should default to false when removed resource.TestCheckResourceAttr(resourceName, "config.write_key", "updated-write-key"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), @@ -468,14 +468,14 @@ func TestAccDestination_UpdateAzureEventHubs(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "azure-event-hubs-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "azure-event-hubs"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "azure-event-hubs-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "azure-event-hubs"), resource.TestCheckResourceAttr(resourceName, "config.namespace", "namespace"), - resource.TestCheckResourceAttr(resourceName, "config.name", "name"), + resource.TestCheckResourceAttr(resourceName, "config.name", NAME), resource.TestCheckResourceAttr(resourceName, "config.policy_name", "policy-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_key", "super-secret-policy-key"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, @@ -484,14 +484,14 @@ func TestAccDestination_UpdateAzureEventHubs(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckDestinationExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "updated-azure-event-hubs-dest"), - resource.TestCheckResourceAttr(resourceName, "kind", "azure-event-hubs"), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "updated-azure-event-hubs-dest"), + resource.TestCheckResourceAttr(resourceName, KIND, "azure-event-hubs"), resource.TestCheckResourceAttr(resourceName, "config.namespace", "namespace"), resource.TestCheckResourceAttr(resourceName, "config.name", "updated-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_name", "updated-policy-name"), resource.TestCheckResourceAttr(resourceName, "config.policy_key", "updated-policy-key"), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), ), }, diff --git a/launchdarkly/resource_launchdarkly_environment_test.go b/launchdarkly/resource_launchdarkly_environment_test.go index e269f4f6..c2eace76 100644 --- a/launchdarkly/resource_launchdarkly_environment_test.go +++ b/launchdarkly/resource_launchdarkly_environment_test.go @@ -115,16 +115,16 @@ func TestAccEnvironment_Create(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "ff00ff"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "true"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "true"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "50"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "ff00ff"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "50"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, "require_comments", "true"), - resource.TestCheckResourceAttr(resourceName, "confirm_changes", "true"), + resource.TestCheckResourceAttr(resourceName, REQUIRE_COMMENTS, "true"), + resource.TestCheckResourceAttr(resourceName, CONFIRM_CHANGES, "true"), resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), resource.TestCheckResourceAttr(resourceName, "tags.0", "tagged"), ), @@ -152,13 +152,13 @@ func TestAccEnvironment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "ff00ff"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "true"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "true"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "50"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "ff00ff"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "50"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), ), }, { @@ -166,15 +166,15 @@ func TestAccEnvironment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "The real staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "000000"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "false"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "false"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "3"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "require_comments", "false"), - resource.TestCheckResourceAttr(resourceName, "confirm_changes", "false"), + resource.TestCheckResourceAttr(resourceName, NAME, "The real staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "000000"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "3"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, REQUIRE_COMMENTS, "false"), + resource.TestCheckResourceAttr(resourceName, CONFIRM_CHANGES, "false"), ), }, }, @@ -195,13 +195,13 @@ func TestAccEnvironment_RemoveAttributes(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "ff00ff"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "true"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "true"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "50"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "ff00ff"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "true"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "50"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), ), }, { @@ -209,15 +209,15 @@ func TestAccEnvironment_RemoveAttributes(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "The real staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "000000"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "false"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "false"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "0"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "require_comments", "false"), - resource.TestCheckResourceAttr(resourceName, "confirm_changes", "false"), + resource.TestCheckResourceAttr(resourceName, NAME, "The real staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "000000"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "0"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, REQUIRE_COMMENTS, "false"), + resource.TestCheckResourceAttr(resourceName, CONFIRM_CHANGES, "false"), ), }, }, @@ -242,15 +242,15 @@ func TestAccEnvironment_Invalid(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "The real staging1"), - resource.TestCheckResourceAttr(resourceName, "key", "staging1"), - resource.TestCheckResourceAttr(resourceName, "color", "000000"), - resource.TestCheckResourceAttr(resourceName, "secure_mode", "false"), - resource.TestCheckResourceAttr(resourceName, "default_track_events", "false"), - resource.TestCheckResourceAttr(resourceName, "default_ttl", "3"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "require_comments", "false"), - resource.TestCheckResourceAttr(resourceName, "confirm_changes", "false"), + resource.TestCheckResourceAttr(resourceName, NAME, "The real staging1"), + resource.TestCheckResourceAttr(resourceName, KEY, "staging1"), + resource.TestCheckResourceAttr(resourceName, COLOR, "000000"), + resource.TestCheckResourceAttr(resourceName, SECURE_MODE, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TRACK_EVENTS, "false"), + resource.TestCheckResourceAttr(resourceName, DEFAULT_TTL, "3"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, REQUIRE_COMMENTS, "false"), + resource.TestCheckResourceAttr(resourceName, CONFIRM_CHANGES, "false"), ), }, }, @@ -271,10 +271,10 @@ func TestAccEnvironmentWithApprovals(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test"), - resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), - resource.TestCheckResourceAttr(resourceName, "color", "ababab"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Approvals Test"), + resource.TestCheckResourceAttr(resourceName, KEY, "approvals-test"), + resource.TestCheckResourceAttr(resourceName, COLOR, "ababab"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "false"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "true"), // should default to true resource.TestCheckResourceAttr(resourceName, "approval_settings.0.min_num_approvals", "2"), @@ -291,10 +291,10 @@ func TestAccEnvironmentWithApprovals(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test 2.0"), - resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), - resource.TestCheckResourceAttr(resourceName, "color", "bababa"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Approvals Test 2.0"), + resource.TestCheckResourceAttr(resourceName, KEY, "approvals-test"), + resource.TestCheckResourceAttr(resourceName, COLOR, "bababa"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.required", "true"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_review_own_request", "true"), resource.TestCheckResourceAttr(resourceName, "approval_settings.0.can_apply_declined_changes", "false"), @@ -312,11 +312,11 @@ func TestAccEnvironmentWithApprovals(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Approvals Test 2.1"), - resource.TestCheckResourceAttr(resourceName, "key", "approvals-test"), - resource.TestCheckResourceAttr(resourceName, "color", "bababa"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckNoResourceAttr(resourceName, "approval_settings"), + resource.TestCheckResourceAttr(resourceName, NAME, "Approvals Test 2.1"), + resource.TestCheckResourceAttr(resourceName, KEY, "approvals-test"), + resource.TestCheckResourceAttr(resourceName, COLOR, "bababa"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckNoResourceAttr(resourceName, APPROVAL_SETTINGS), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go index cdeaf12f..20af0633 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment_test.go @@ -366,10 +366,10 @@ func TestAccFeatureFlagEnvironment_Basic(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "2"), resource.TestCheckResourceAttr(resourceName, "targets.#", "1"), resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "0"), @@ -397,15 +397,15 @@ func TestAccFeatureFlagEnvironment_Empty(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentEmpty), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "2"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "track_events", "false"), - resource.TestCheckNoResourceAttr(resourceName, "rules"), + resource.TestCheckResourceAttr(resourceName, TRACK_EVENTS, "false"), + resource.TestCheckNoResourceAttr(resourceName, RULES), resource.TestCheckNoResourceAttr(resourceName, "rules.#"), - resource.TestCheckNoResourceAttr(resourceName, "prerequisites"), + resource.TestCheckNoResourceAttr(resourceName, PREREQUISITES), resource.TestCheckNoResourceAttr(resourceName, "prerequisites.#"), - resource.TestCheckNoResourceAttr(resourceName, "targets"), + resource.TestCheckNoResourceAttr(resourceName, TARGETS), resource.TestCheckNoResourceAttr(resourceName, "targets.#"), ), }, @@ -431,7 +431,7 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBasic), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout.#", "0"), @@ -439,15 +439,15 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "targets.0.values.0", "user1"), resource.TestCheckResourceAttr(resourceName, "targets.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "rules.#", "0"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "2"), ), }, { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentUpdate), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "true"), - resource.TestCheckResourceAttr(resourceName, "track_events", "true"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), + resource.TestCheckResourceAttr(resourceName, TRACK_EVENTS, "true"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.rollout_weights.#", "3"), @@ -481,7 +481,7 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.values.0", "h"), resource.TestCheckResourceAttr(resourceName, "rules.1.clauses.0.negate", "false"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "1"), ), }, // After changes have been made to the resource, removing optional values should revert to their default / null values. @@ -489,10 +489,10 @@ func TestAccFeatureFlagEnvironment_Update(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentEmpty), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), - resource.TestCheckResourceAttr(resourceName, "track_events", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), + resource.TestCheckResourceAttr(resourceName, TRACK_EVENTS, "false"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "2"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "2"), resource.TestCheckNoResourceAttr(resourceName, "targets.#"), resource.TestCheckNoResourceAttr(resourceName, "rules.#"), ), @@ -519,10 +519,10 @@ func TestAccFeatureFlagEnvironment_JSON_variations(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentJSONVariations), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "fallthrough.#", "1"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "0"), ), }, { @@ -548,14 +548,14 @@ func TestAccFeatureFlagEnvironment_BoolClauseValue(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentBoolClauseValue), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.value_type", "boolean"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", "true"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "1"), ), }, { @@ -580,7 +580,7 @@ func TestAccFeatureFlagEnvironment_NumberClauseValue(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentNumberClauseValue), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "rules.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.value_type", "number"), @@ -588,7 +588,7 @@ func TestAccFeatureFlagEnvironment_NumberClauseValue(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.0", "42"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.values.1", "84"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "0"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "1"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "1"), ), }, { @@ -634,21 +634,21 @@ func TestAccFeatureFlagEnvironment_Prereq(t *testing.T) { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentPrereq), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "prerequisites.#", "1"), resource.TestCheckResourceAttr(resourceName, "prerequisites.0.flag_key", "bool-flag"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "0"), ), }, { Config: withRandomProject(projectKey, testAccFeatureFlagEnvironmentRemovePrereq), Check: resource.ComposeTestCheckFunc( testAccCheckFeatureFlagEnvironmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckNoResourceAttr(resourceName, "prerequisites.#"), resource.TestCheckResourceAttr(resourceName, "fallthrough.0.variation", "1"), - resource.TestCheckResourceAttr(resourceName, "off_variation", "0"), + resource.TestCheckResourceAttr(resourceName, OFF_VARIATION, "0"), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index 92ba1c86..e5d82a49 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -513,7 +513,7 @@ func TestAccFeatureFlag_Basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), ), }, { @@ -540,9 +540,9 @@ func TestAccFeatureFlag_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "basic-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), ), }, { @@ -550,15 +550,15 @@ func TestAccFeatureFlag_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Less basic feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "basic-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "description", "this is a boolean flag by default becausethe variations field is omitted"), + resource.TestCheckResourceAttr(resourceName, NAME, "Less basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "this is a boolean flag by default becausethe variations field is omitted"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "tags.1", "update"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), - resource.TestCheckResourceAttr(resourceName, "temporary", "true"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), + resource.TestCheckResourceAttr(resourceName, TEMPORARY, "true"), resource.TestCheckResourceAttr(resourceName, "defaults.0.on_variation", "0"), resource.TestCheckResourceAttr(resourceName, "defaults.0.off_variation", "1"), ), @@ -670,10 +670,10 @@ func TestAccFeatureFlag_WithMaintainer(t *testing.T) { testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckMemberExists("launchdarkly_team_member.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttrPair(resourceName, "maintainer_id", "launchdarkly_team_member.test", "id"), + resource.TestCheckResourceAttr(resourceName, NAME, "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttrPair(resourceName, MAINTAINER_ID, "launchdarkly_team_member.test", "id"), ), }, { @@ -681,11 +681,11 @@ func TestAccFeatureFlag_WithMaintainer(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), // when removed it should reset back to the most recently-set maintainer - resource.TestCheckResourceAttrPair(resourceName, "maintainer_id", "launchdarkly_team_member.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, MAINTAINER_ID, "launchdarkly_team_member.test", "id"), ), }, { @@ -693,12 +693,12 @@ func TestAccFeatureFlag_WithMaintainer(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), // it will still be set to the most recently set one even if that member has been deleted // the UI will not show a maintainer because it will not be able to find the record post-member delete - resource.TestCheckResourceAttrSet(resourceName, "maintainer_id"), + resource.TestCheckResourceAttrSet(resourceName, MAINTAINER_ID), ), }, }, @@ -727,10 +727,10 @@ func TestAccFeatureFlag_InvalidMaintainer(t *testing.T) { testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckMemberExists("launchdarkly_team_member.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttrPair(resourceName, "maintainer_id", "launchdarkly_team_member.test", "id"), + resource.TestCheckResourceAttr(resourceName, NAME, "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttrPair(resourceName, MAINTAINER_ID, "launchdarkly_team_member.test", "id"), ), }, { @@ -738,12 +738,12 @@ func TestAccFeatureFlag_InvalidMaintainer(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Maintained feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "maintained-flag"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "Maintained feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "maintained-flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), // this is the best we can do. it should default back to the most recently-set maintainer but // we have no easy way of a - resource.TestCheckResourceAttrSet(resourceName, "maintainer_id"), + resource.TestCheckResourceAttrSet(resourceName, MAINTAINER_ID), ), }, }, @@ -764,10 +764,10 @@ func TestAccFeatureFlag_CreateMultivariate(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "multivariate flag 1 name"), - resource.TestCheckResourceAttr(resourceName, "key", "multivariate-flag-1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "description", "this is a multivariate flag because we explicitly define the variations"), + resource.TestCheckResourceAttr(resourceName, NAME, "multivariate flag 1 name"), + resource.TestCheckResourceAttr(resourceName, KEY, "multivariate-flag-1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "this is a multivariate flag because we explicitly define the variations"), resource.TestCheckResourceAttr(resourceName, "variations.#", "3"), resource.TestCheckResourceAttr(resourceName, "variations.0.description", "a description"), resource.TestCheckResourceAttr(resourceName, "variations.0.name", "variation1"), @@ -810,11 +810,11 @@ func TestAccFeatureFlag_CreateMultivariate2(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "multivariate flag 2 name"), - resource.TestCheckResourceAttr(resourceName, "key", "multivariate-flag-2"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "description", "this is a multivariate flag to test big number values"), - resource.TestCheckResourceAttr(resourceName, "variation_type", "number"), + resource.TestCheckResourceAttr(resourceName, NAME, "multivariate flag 2 name"), + resource.TestCheckResourceAttr(resourceName, KEY, "multivariate-flag-2"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "this is a multivariate flag to test big number values"), + resource.TestCheckResourceAttr(resourceName, VARIATION_TYPE, "number"), resource.TestCheckResourceAttr(resourceName, "variations.#", "3"), resource.TestCheckResourceAttr(resourceName, "variations.0.description", "a description"), resource.TestCheckResourceAttr(resourceName, "variations.0.name", "variation1"), @@ -869,10 +869,10 @@ func TestAccFeatureFlag_UpdateMultivariate(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "multivariate flag 1 name"), - resource.TestCheckResourceAttr(resourceName, "key", "multivariate-flag-1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "description", "this is a multivariate flag because we explicitly define the variations"), + resource.TestCheckResourceAttr(resourceName, NAME, "multivariate flag 1 name"), + resource.TestCheckResourceAttr(resourceName, KEY, "multivariate-flag-1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "this is a multivariate flag because we explicitly define the variations"), resource.TestCheckResourceAttr(resourceName, "variations.#", "4"), resource.TestCheckResourceAttr(resourceName, "variations.0.description", "a description"), resource.TestCheckResourceAttr(resourceName, "variations.0.name", "variation1"), @@ -1052,7 +1052,7 @@ func TestAccFeatureFlag_ClientSideAvailabilityUpdate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), ), @@ -1069,7 +1069,7 @@ func TestAccFeatureFlag_ClientSideAvailabilityUpdate(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), ), @@ -1099,8 +1099,8 @@ func TestAccFeatureFlag_IncludeInSnippetToClientSide(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), ), }, { @@ -1115,10 +1115,10 @@ func TestAccFeatureFlag_IncludeInSnippetToClientSide(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), ), }, { @@ -1133,10 +1133,10 @@ func TestAccFeatureFlag_IncludeInSnippetToClientSide(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), ), }, }, @@ -1164,10 +1164,10 @@ func TestAccFeatureFlag_ClientSideToIncludeInSnippet(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "true"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "true"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), ), }, { @@ -1182,10 +1182,10 @@ func TestAccFeatureFlag_ClientSideToIncludeInSnippet(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "variations.#", "2"), resource.TestCheckResourceAttr(resourceName, "variations.0.value", "true"), resource.TestCheckResourceAttr(resourceName, "variations.1.value", "false"), - resource.TestCheckNoResourceAttr(resourceName, "maintainer_id"), + resource.TestCheckNoResourceAttr(resourceName, MAINTAINER_ID), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "client_side_availability.0.using_mobile_key", "false"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), ), }, }, @@ -1207,10 +1207,10 @@ func TestAccFeatureFlag_IncludeInSnippetRevertToDefault(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), ), }, // Replace default value with specific value @@ -1219,10 +1219,10 @@ func TestAccFeatureFlag_IncludeInSnippetRevertToDefault(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), ), }, // Clear specific value, check for default @@ -1231,10 +1231,10 @@ func TestAccFeatureFlag_IncludeInSnippetRevertToDefault(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckFeatureFlagExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Basic feature flag"), - resource.TestCheckResourceAttr(resourceName, "key", "basic-flag-sdk-settings"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Basic feature flag"), + resource.TestCheckResourceAttr(resourceName, KEY, "basic-flag-sdk-settings"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), ), }, }, diff --git a/launchdarkly/resource_launchdarkly_project_test.go b/launchdarkly/resource_launchdarkly_project_test.go index 24d3c44d..581297b4 100644 --- a/launchdarkly/resource_launchdarkly_project_test.go +++ b/launchdarkly/resource_launchdarkly_project_test.go @@ -166,8 +166,8 @@ func TestAccProject_Create(t *testing.T) { Config: fmt.Sprintf(testAccProjectCreate, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "tags.1", "test"), @@ -195,10 +195,10 @@ func TestAccProject_Update(t *testing.T) { Config: fmt.Sprintf(testAccProjectCreate, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "tags.1", "test"), resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), @@ -211,9 +211,9 @@ func TestAccProject_Update(t *testing.T) { Config: fmt.Sprintf(testAccProjectUpdate, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "awesome test project"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), @@ -226,11 +226,11 @@ func TestAccProject_Update(t *testing.T) { Config: fmt.Sprintf(testAccProjectUpdateRemoveOptional, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "awesome test project"), resource.TestCheckNoResourceAttr(resourceName, "tags"), resource.TestCheckNoResourceAttr(resourceName, "tags.#"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), ), }, { @@ -255,9 +255,9 @@ func TestAccProject_CSA_Update_And_Revert(t *testing.T) { Config: fmt.Sprintf(testAccProjectCreate, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), ), @@ -266,9 +266,9 @@ func TestAccProject_CSA_Update_And_Revert(t *testing.T) { Config: fmt.Sprintf(testAccProjectClientSideAvailabilityTrue, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "true"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "true"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "true"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), ), @@ -277,9 +277,9 @@ func TestAccProject_CSA_Update_And_Revert(t *testing.T) { Config: fmt.Sprintf(testAccProjectUpdateRemoveOptional, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "awesome test project"), - resource.TestCheckResourceAttr(resourceName, "include_in_snippet", "false"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "awesome test project"), + resource.TestCheckResourceAttr(resourceName, INCLUDE_IN_SNIPPET, "false"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_environment_id", "false"), resource.TestCheckResourceAttr(resourceName, "default_client_side_availability.0.using_mobile_key", "true"), ), @@ -306,8 +306,8 @@ func TestAccProject_WithEnvironments(t *testing.T) { Config: fmt.Sprintf(testAccProjectWithEnvironment, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), resource.TestCheckResourceAttr(resourceName, "environments.0.name", "test environment"), resource.TestCheckResourceAttr(resourceName, "environments.0.tags.#", "2"), @@ -330,8 +330,8 @@ func TestAccProject_WithEnvironments(t *testing.T) { Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdate, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "environments.#", "2"), // Check environment 0 was updated @@ -368,8 +368,8 @@ func TestAccProject_WithEnvironments(t *testing.T) { Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdateApprovalSettings, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "environments.#", "2"), // Check approval_settings have updated as expected @@ -397,8 +397,8 @@ func TestAccProject_WithEnvironments(t *testing.T) { Config: fmt.Sprintf(testAccProjectWithEnvironmentUpdateRemove, projectKey), Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", projectKey), - resource.TestCheckResourceAttr(resourceName, "name", "test project"), + resource.TestCheckResourceAttr(resourceName, KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, NAME, "test project"), resource.TestCheckResourceAttr(resourceName, "environments.#", "1"), // Check that optional attributes defaulted back to false diff --git a/launchdarkly/resource_launchdarkly_segment_test.go b/launchdarkly/resource_launchdarkly_segment_test.go index 4db7d684..ac52de8a 100644 --- a/launchdarkly/resource_launchdarkly_segment_test.go +++ b/launchdarkly/resource_launchdarkly_segment_test.go @@ -97,11 +97,11 @@ func TestAccSegment_Create(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckSegmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", "segmentKey1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment name"), - resource.TestCheckResourceAttr(resourceName, "description", "segment description"), + resource.TestCheckResourceAttr(resourceName, KEY, "segmentKey1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment name"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), @@ -111,7 +111,7 @@ func TestAccSegment_Create(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "excluded.#", "2"), resource.TestCheckResourceAttr(resourceName, "excluded.0", "user3"), resource.TestCheckResourceAttr(resourceName, "excluded.1", "user4"), - resource.TestCheckResourceAttrSet(resourceName, "creation_date"), + resource.TestCheckResourceAttrSet(resourceName, CREATION_DATE), ), }, { @@ -137,11 +137,11 @@ func TestAccSegment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckSegmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", "segmentKey1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment name"), - resource.TestCheckResourceAttr(resourceName, "description", "segment description"), + resource.TestCheckResourceAttr(resourceName, KEY, "segmentKey1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment name"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), @@ -158,11 +158,11 @@ func TestAccSegment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckSegmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", "segmentKey1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment name"), - resource.TestCheckResourceAttr(resourceName, "description", "segment description"), + resource.TestCheckResourceAttr(resourceName, KEY, "segmentKey1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment name"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", ".segmentTag2"), resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag1"), @@ -194,11 +194,11 @@ func TestAccSegment_Update(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckSegmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", "segmentKey1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment name"), - resource.TestCheckResourceAttr(resourceName, "description", "segment description"), + resource.TestCheckResourceAttr(resourceName, KEY, "segmentKey1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment name"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "segment description"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "segmentTag1"), resource.TestCheckResourceAttr(resourceName, "tags.1", "segmentTag2"), @@ -208,7 +208,7 @@ func TestAccSegment_Update(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "excluded.#", "2"), resource.TestCheckResourceAttr(resourceName, "excluded.0", "user3"), resource.TestCheckResourceAttr(resourceName, "excluded.1", "user4"), - resource.TestCheckNoResourceAttr(resourceName, "rules"), + resource.TestCheckNoResourceAttr(resourceName, RULES), ), }, { @@ -234,11 +234,11 @@ func TestAccSegment_WithRules(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckProjectExists("launchdarkly_project.test"), testAccCheckSegmentExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "key", "segmentKey1"), - resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), - resource.TestCheckResourceAttr(resourceName, "env_key", "test"), - resource.TestCheckResourceAttr(resourceName, "name", "segment name"), - resource.TestCheckResourceAttr(resourceName, "description", "segment description"), + resource.TestCheckResourceAttr(resourceName, KEY, "segmentKey1"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, NAME, "segment name"), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "segment description"), resource.TestCheckResourceAttr(resourceName, "rules.#", "2"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.#", "1"), resource.TestCheckResourceAttr(resourceName, "rules.0.clauses.0.attribute", "test_att"), diff --git a/launchdarkly/resource_launchdarkly_team_member_test.go b/launchdarkly/resource_launchdarkly_team_member_test.go index 8bfec216..7dab9a29 100644 --- a/launchdarkly/resource_launchdarkly_team_member_test.go +++ b/launchdarkly/resource_launchdarkly_team_member_test.go @@ -93,10 +93,10 @@ func TestAccTeamMember_CreateGeneric(t *testing.T) { Config: fmt.Sprintf(testAccTeamMemberCreate, randomName), Check: resource.ComposeTestCheckFunc( testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), - resource.TestCheckResourceAttr(resourceName, "role", "admin"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), + resource.TestCheckResourceAttr(resourceName, ROLE, "admin"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"), ), }, @@ -122,10 +122,10 @@ func TestAccTeamMember_UpdateGeneric(t *testing.T) { Config: fmt.Sprintf(testAccTeamMemberCreate, randomName), Check: resource.ComposeTestCheckFunc( testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), - resource.TestCheckResourceAttr(resourceName, "role", "admin"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), + resource.TestCheckResourceAttr(resourceName, ROLE, "admin"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"), ), }, @@ -133,10 +133,10 @@ func TestAccTeamMember_UpdateGeneric(t *testing.T) { Config: fmt.Sprintf(testAccTeamMemberUpdate, randomName), Check: resource.ComposeTestCheckFunc( testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), - resource.TestCheckResourceAttr(resourceName, "role", "writer"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), + resource.TestCheckResourceAttr(resourceName, ROLE, "writer"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "0"), ), }, @@ -160,9 +160,9 @@ func TestAccTeamMember_CreateWithCustomRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(roleResourceName), testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey), ), @@ -194,9 +194,9 @@ func TestAccTeamMember_UpdateWithCustomRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(roleResourceName1), testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey1), ), @@ -211,9 +211,9 @@ func TestAccTeamMember_UpdateWithCustomRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckCustomRoleExists(roleResourceName2), testAccCheckMemberExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "email", fmt.Sprintf("%s@example.com", randomName)), - resource.TestCheckResourceAttr(resourceName, "first_name", "first"), - resource.TestCheckResourceAttr(resourceName, "last_name", "last"), + resource.TestCheckResourceAttr(resourceName, EMAIL, fmt.Sprintf("%s@example.com", randomName)), + resource.TestCheckResourceAttr(resourceName, FIRST_NAME, "first"), + resource.TestCheckResourceAttr(resourceName, LAST_NAME, "last"), resource.TestCheckResourceAttr(resourceName, "custom_roles.#", "1"), resource.TestCheckResourceAttr(resourceName, "custom_roles.0", roleKey2), ), diff --git a/launchdarkly/resource_launchdarkly_webhook_test.go b/launchdarkly/resource_launchdarkly_webhook_test.go index b88783a3..87d602d9 100644 --- a/launchdarkly/resource_launchdarkly_webhook_test.go +++ b/launchdarkly/resource_launchdarkly_webhook_test.go @@ -117,9 +117,9 @@ func TestAccWebhook_Create(t *testing.T) { Config: testAccWebhookCreate, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "example-webhook"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), @@ -146,9 +146,9 @@ func TestAccWebhook_CreateWithEnabled(t *testing.T) { Config: testAccWebhookCreateWithEnabled, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "example-webhook"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), @@ -174,9 +174,9 @@ func TestAccWebhook_Update(t *testing.T) { Config: testAccWebhookCreateWithEnabled, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "example-webhook"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), @@ -186,9 +186,9 @@ func TestAccWebhook_Update(t *testing.T) { Config: testAccWebhookCreate, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "example-webhook"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "example-webhook"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), @@ -198,9 +198,9 @@ func TestAccWebhook_Update(t *testing.T) { Config: testAccWebhookUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Example Webhook"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com/updatedUrl"), - resource.TestCheckResourceAttr(resourceName, "on", "false"), + resource.TestCheckResourceAttr(resourceName, NAME, "Example Webhook"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com/updatedUrl"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), resource.TestCheckResourceAttr(resourceName, "tags.0", "terraform"), resource.TestCheckResourceAttr(resourceName, "tags.1", "updated"), @@ -228,9 +228,9 @@ func TestAccWebhook_CreateWithStatements(t *testing.T) { Config: testAccWebhookWithStatements, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Webhook with policy statements"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Webhook with policy statements"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), @@ -260,9 +260,9 @@ func TestAccWebhook_CreateWithPolicyStatements(t *testing.T) { Config: testAccWebhookWithPolicyStatements, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Webhook with policy statements"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Webhook with policy statements"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), @@ -287,9 +287,9 @@ func TestAccWebhook_UpdateWithStatements(t *testing.T) { Config: testAccWebhookWithStatements, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Webhook with policy statements"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Webhook with policy statements"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), @@ -302,9 +302,9 @@ func TestAccWebhook_UpdateWithStatements(t *testing.T) { Config: testAccWebhookWithPolicyUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Webhook with policy statements"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Webhook with policy statements"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "statements.#", "2"), resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), @@ -322,9 +322,9 @@ func TestAccWebhook_UpdateWithStatements(t *testing.T) { Config: testAccWebhookWithStatementsRemoved, Check: resource.ComposeTestCheckFunc( testAccCheckWebhookExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "name", "Webhook without statements"), - resource.TestCheckResourceAttr(resourceName, "url", "http://webhooks.com"), - resource.TestCheckResourceAttr(resourceName, "on", "true"), + resource.TestCheckResourceAttr(resourceName, NAME, "Webhook without statements"), + resource.TestCheckResourceAttr(resourceName, URL, "http://webhooks.com"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), resource.TestCheckResourceAttr(resourceName, "statements.#", "0"), ), }, From 9cb5f1af94d3fa6998554cb3626da144acc64063 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Thu, 6 Jan 2022 23:24:59 -0600 Subject: [PATCH 19/36] Added validation to project key length --- launchdarkly/resource_launchdarkly_project.go | 2 +- launchdarkly/validation_helper.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index 20deb29a..ce08564f 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -64,7 +64,7 @@ func resourceProject() *schema.Resource { Required: true, Description: "The project's unique key", ForceNew: true, - ValidateFunc: validateKey(), + ValidateFunc: validateKeyAndLength(1, 20), }, NAME: { Type: schema.TypeString, diff --git a/launchdarkly/validation_helper.go b/launchdarkly/validation_helper.go index 46ddf14e..9aca7cc9 100644 --- a/launchdarkly/validation_helper.go +++ b/launchdarkly/validation_helper.go @@ -14,6 +14,16 @@ func validateKey() schema.SchemaValidateFunc { ) } +func validateKeyAndLength(minLength, maxLength int) schema.SchemaValidateFunc { + return validation.All( + validation.StringMatch( + regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`), + "Must contain only letters, numbers, '.', '-', or '_' and must start with an alphanumeric", + ), + validation.StringLenBetween(minLength, maxLength), + ) +} + func validateID() schema.SchemaValidateFunc { return validation.All( validation.StringMatch(regexp.MustCompile(`^[a-fA-F0-9]*$`), "Must be a 24 character hexadecimal string"), From 90966978c5287ca31b01136da0fb6c0efa47cc8d Mon Sep 17 00:00:00 2001 From: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> Date: Mon, 10 Jan 2022 04:36:36 -0600 Subject: [PATCH 20/36] Added rate limit and conflict retry logic to LD client (#171) * Added rate limit and conflicts retry logic to LD client * Removed occurences of handleRateLimit and handleConflicts from the codebase since both logic has been replaced by go-retyable * Used retryable client for the fallbackClient, fixed a header bug in the unit tests and added jitter to sleep value for 429s Co-authored-by: Henry Barrow --- go.mod | 1 + go.sum | 3 + launchdarkly/config.go | 64 +++++- launchdarkly/config_test.go | 211 ++++++++++++++++++ ...nchdarkly_feature_flag_environment_test.go | 18 +- .../data_source_launchdarkly_segment_test.go | 18 +- .../data_source_launchdarkly_team_member.go | 14 +- .../data_source_launchdarkly_webhook_test.go | 17 +- launchdarkly/environments_helper.go | 7 +- .../feature_flag_environment_helper.go | 6 +- launchdarkly/feature_flags_helper.go | 13 +- launchdarkly/helper.go | 52 ----- launchdarkly/helper_test.go | 125 ----------- launchdarkly/project_helper.go | 8 +- .../resource_launchdarkly_access_token.go | 22 +- .../resource_launchdarkly_custom_role.go | 23 +- .../resource_launchdarkly_destination.go | 28 +-- .../resource_launchdarkly_environment.go | 21 +- .../resource_launchdarkly_feature_flag.go | 18 +- ...e_launchdarkly_feature_flag_environment.go | 19 +- launchdarkly/resource_launchdarkly_project.go | 41 +--- launchdarkly/resource_launchdarkly_segment.go | 22 +- .../resource_launchdarkly_team_member.go | 22 +- launchdarkly/resource_launchdarkly_webhook.go | 22 +- launchdarkly/segments_helper.go | 7 +- launchdarkly/team_member_helper.go | 13 +- launchdarkly/test_utils.go | 21 +- launchdarkly/webhooks_helper.go | 7 +- 28 files changed, 359 insertions(+), 484 deletions(-) create mode 100644 launchdarkly/config_test.go delete mode 100644 launchdarkly/helper_test.go diff --git a/go.mod b/go.mod index 92b4b1c6..2c4d670f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v1.0.0 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect + github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/hcl/v2 v2.11.1 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 // indirect diff --git a/go.sum b/go.sum index fe8b8993..c306e80b 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,7 @@ github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBM github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ3ROU= github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo= @@ -201,6 +202,8 @@ github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYt github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/launchdarkly/config.go b/launchdarkly/config.go index a69c0f9b..a6b364dc 100644 --- a/launchdarkly/config.go +++ b/launchdarkly/config.go @@ -4,9 +4,13 @@ import ( "context" "errors" "fmt" + "log" + "math" "net/http" + "strconv" "time" + retryablehttp "github.com/hashicorp/go-retryablehttp" ldapi "github.com/launchdarkly/api-client-go/v7" ) @@ -14,7 +18,10 @@ import ( var version = "unreleased" const ( - APIVersion = "20191212" + APIVersion = "20191212" + MAX_RETRIES = 8 + RETRY_WAIT_MIN = 200 * time.Millisecond + RETRY_WAIT_MAX = 2000 * time.Millisecond ) // Client is used by the provider to access the ld API. @@ -35,6 +42,7 @@ func newClient(token string, apiHost string, oauth bool) (*Client, error) { cfg.Host = apiHost cfg.DefaultHeader = make(map[string]string) cfg.UserAgent = fmt.Sprintf("launchdarkly-terraform-provider/%s", version) + cfg.HTTPClient = newRetryableClient() cfg.AddDefaultHeader("LD-API-Version", APIVersion) @@ -47,15 +55,61 @@ func newClient(token string, apiHost string, oauth bool) (*Client, error) { } // TODO: remove this once we get the go client reset endpoint fixed - fallbackClient := http.Client{ - Timeout: time.Duration(5 * time.Second), - } + fallbackClient := newRetryableClient() + fallbackClient.Timeout = time.Duration(5 * time.Second) return &Client{ apiKey: token, apiHost: apiHost, ld: ldapi.NewAPIClient(cfg), ctx: ctx, - fallbackClient: &fallbackClient, + fallbackClient: fallbackClient, }, nil } + +func newRetryableClient() *http.Client { + retryClient := retryablehttp.NewClient() + retryClient.RetryWaitMin = RETRY_WAIT_MIN + retryClient.RetryWaitMax = RETRY_WAIT_MAX + retryClient.Backoff = backOff + retryClient.CheckRetry = retryPolicy + retryClient.RetryMax = MAX_RETRIES + retryClient.ErrorHandler = retryablehttp.PassthroughErrorHandler + + return retryClient.StandardClient() +} + +func backOff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration { + if resp != nil && resp.StatusCode == http.StatusTooManyRequests { + sleepStr := resp.Header.Get("X-RateLimit-Reset") + if sleep, err := strconv.ParseInt(sleepStr, 10, 64); err == nil { + resetTime := time.Unix(0, sleep*int64(time.Millisecond)) + sleepDuration := time.Until(resetTime) + + // We have observed situations where LD-s retry header results in a negative sleep duration. In this case, + // multiply the duration by -1 and add jitter + if sleepDuration <= 0 { + log.Printf("[DEBUG] received a negative rate limit retry duration of %s.", sleepDuration) + sleepDuration = -1 * sleepDuration + } + + return sleepDuration + getRandomSleepDuration(sleepDuration) + } + } + + backoffTime := math.Pow(2, float64(attemptNum)) * float64(min) + sleep := time.Duration(backoffTime) + if float64(sleep) != backoffTime || sleep > max { + sleep = max + } + return sleep +} + +func retryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) { + retry, retryErr := retryablehttp.DefaultRetryPolicy(ctx, resp, err) + if !retry && retryErr == nil && err == nil && resp.StatusCode == http.StatusConflict { + return true, nil + } + + return retry, retryErr +} diff --git a/launchdarkly/config_test.go b/launchdarkly/config_test.go new file mode 100644 index 00000000..a6d75962 --- /dev/null +++ b/launchdarkly/config_test.go @@ -0,0 +1,211 @@ +package launchdarkly + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandleRateLimits(t *testing.T) { + t.Run("no retries needed", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, calls, 1) + }) + + t.Run("max retries exceeded", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) + w.WriteHeader(http.StatusTooManyRequests) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusTooManyRequests) + assert.Equal(t, calls, MAX_RETRIES+1) + }) + + t.Run("retry resolved with header", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + + if calls == 3 { + w.WriteHeader(http.StatusOK) + return + } + + w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) + w.WriteHeader(http.StatusTooManyRequests) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, 3, calls) + }) + + t.Run("retry resolved with negative header", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + + if calls == 3 { + w.WriteHeader(http.StatusOK) + return + } + + w.Header().Add("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(-100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) + w.WriteHeader(http.StatusTooManyRequests) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, 3, calls) + }) + + t.Run("retry resolved without header", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + + if calls == 3 { + w.WriteHeader(http.StatusOK) + return + } + + w.WriteHeader(http.StatusTooManyRequests) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, 3, calls) + }) +} + +func TestHandleConflicts(t *testing.T) { + t.Run("no retries needed", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, calls, 1) + }) + + t.Run("max retries exceeded", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(http.StatusConflict) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusConflict) + assert.Equal(t, calls, MAX_RETRIES+1) + }) + + t.Run("conflict resolved", func(t *testing.T) { + t.Parallel() + calls := 0 + + // create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + + if calls == 3 { + w.WriteHeader(http.StatusOK) + return + } + + w.WriteHeader(http.StatusConflict) + })) + defer ts.Close() + + // create a client + client, err := newClient("token", ts.URL, false) + require.NoError(t, err) + + res, err := client.ld.GetConfig().HTTPClient.Get(ts.URL) + require.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, 3, calls) + }) +} diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go index 400e8e61..3309ab3b 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment_test.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "os" "testing" @@ -41,30 +40,21 @@ func testAccDataSourceFeatureFlagEnvironmentScaffold(client *Client, projectKey, patch := ldapi.NewPatchWithComment(envConfigPatches) patch.SetComment("Terraform feature flag env data source test") - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(*patch).Execute() - }) - }) + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(*patch).Execute() + if err != nil { // delete project if anything fails because otherwise we will see a // 409 error later and have to clean it up manually _ = testAccDataSourceProjectDelete(client, projectKey) return nil, fmt.Errorf("failed to create feature flag env config: %s", err.Error()) } - flagRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() - }) + flag, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() + if err != nil { _ = testAccDataSourceProjectDelete(client, projectKey) return nil, fmt.Errorf("failed to get feature flag: %s", err.Error()) } - flag, ok := flagRaw.(ldapi.FeatureFlag) - if !ok { - _ = testAccDataSourceProjectDelete(client, projectKey) - return nil, fmt.Errorf("failed to create feature flag env config") - } return &flag, nil } diff --git a/launchdarkly/data_source_launchdarkly_segment_test.go b/launchdarkly/data_source_launchdarkly_segment_test.go index 9562b0d8..c5c0189a 100644 --- a/launchdarkly/data_source_launchdarkly_segment_test.go +++ b/launchdarkly/data_source_launchdarkly_segment_test.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "os" "regexp" "testing" @@ -46,9 +45,8 @@ func testAccDataSourceSegmentCreate(client *Client, projectKey, segmentKey strin Description: ldapi.PtrString("test description"), Tags: &[]string{"terraform"}, } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.PostSegment(client.ctx, project.Key, envKey).SegmentBody(segmentBody).Execute() - }) + _, _, err = client.ld.SegmentsApi.PostSegment(client.ctx, project.Key, envKey).SegmentBody(segmentBody).Execute() + if err != nil { return nil, fmt.Errorf("failed to create segment %q in project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) } @@ -60,19 +58,13 @@ func testAccDataSourceSegmentCreate(client *Client, projectKey, segmentKey strin patchReplace("/rules", properties.Rules), }, } - rawSegment, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, segmentKey).PatchWithComment(patch).Execute() - }) - }) + segment, _, err := client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, segmentKey).PatchWithComment(patch).Execute() + if err != nil { return nil, fmt.Errorf("failed to update segment %q in project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) } - if segment, ok := rawSegment.(ldapi.UserSegment); ok { - return &segment, nil - } - return nil, fmt.Errorf("failed to create segment %q in project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) + return &segment, nil } func TestAccDataSourceSegment_noMatchReturnsError(t *testing.T) { diff --git a/launchdarkly/data_source_launchdarkly_team_member.go b/launchdarkly/data_source_launchdarkly_team_member.go index 821d88ba..9850e17d 100644 --- a/launchdarkly/data_source_launchdarkly_team_member.go +++ b/launchdarkly/data_source_launchdarkly_team_member.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -43,27 +42,24 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er // this should be the max limit allowed when the member-list-max-limit flag is on teamMemberLimit := int64(1000) - membersRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Execute() - }) + members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Execute() + if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) } - members := membersRaw.(ldapi.Members) totalMemberCount := int(*members.TotalCount) memberItems := members.Items membersPulled := len(memberItems) for membersPulled < totalMemberCount { offset := int64(membersPulled) - newRawMembers, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Execute() - }) + newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Execute() + if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) } - newMembers := newRawMembers.(ldapi.Members) + memberItems = append(memberItems, newMembers.Items...) membersPulled = len(memberItems) } diff --git a/launchdarkly/data_source_launchdarkly_webhook_test.go b/launchdarkly/data_source_launchdarkly_webhook_test.go index bbfa630d..4255e41e 100644 --- a/launchdarkly/data_source_launchdarkly_webhook_test.go +++ b/launchdarkly/data_source_launchdarkly_webhook_test.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "os" "regexp" "testing" @@ -36,24 +35,18 @@ func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi. }, }, } - webhookRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() - }) + webhook, _, err := client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() + if err != nil { return nil, fmt.Errorf("failed to create webhook with name %q: %s", webhookName, handleLdapiErr(err)) } - if webhook, ok := webhookRaw.(ldapi.Webhook); ok { - return &webhook, nil - } - return nil, fmt.Errorf("failed to create webhook") + return &webhook, nil } func testAccDataSourceWebhookDelete(client *Client, webhookId string) error { - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookId).Execute() - return nil, res, err - }) + _, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookId).Execute() + if err != nil { return fmt.Errorf("failed to delete webhook with id %q: %s", webhookId, handleLdapiErr(err)) } diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index 2703fb4c..d6449138 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -227,9 +226,8 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) - envRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projectKey, key).Execute() - }) + env, res, err := client.ld.EnvironmentsApi.GetEnvironment(client.ctx, projectKey, key).Execute() + if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find environment with key %q in project %q, removing from state", key, projectKey) d.SetId("") @@ -239,7 +237,6 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool return fmt.Errorf("failed to get environment with key %q for project key: %q: %v", key, projectKey, handleLdapiErr(err)) } - env := envRaw.(ldapi.Environment) d.SetId(projectKey + "/" + key) _ = d.Set(KEY, env.Key) _ = d.Set(NAME, env.Name) diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index 21dbd0ab..b228a623 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -55,11 +55,7 @@ func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Sch // get FeatureFlagEnvironment uses a query parameter to get the ldapi.FeatureFlag with only a single environment. func getFeatureFlagEnvironment(client *Client, projectKey, flagKey, environmentKey string) (ldapi.FeatureFlag, *http.Response, error) { - flagRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Env(environmentKey).Execute() - }) - flag := flagRaw.(ldapi.FeatureFlag) - return flag, res, err + return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Env(environmentKey).Execute() } func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) error { diff --git a/launchdarkly/feature_flags_helper.go b/launchdarkly/feature_flags_helper.go index 2122d5b4..582b6a67 100644 --- a/launchdarkly/feature_flags_helper.go +++ b/launchdarkly/feature_flags_helper.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -114,10 +113,8 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) - flagRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key).Execute() - }) - flag := flagRaw.(ldapi.FeatureFlag) + flag, res, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key).Execute() + if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] feature flag %q in project %q not found, removing from state", key, projectKey) d.SetId("") @@ -206,12 +203,10 @@ func flagIdToKeys(id string) (projectKey string, flagKey string, err error) { } func getProjectDefaultCSAandIncludeInSnippet(client *Client, projectKey string) (ldapi.ClientSideAvailability, bool, error) { - rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() - }) + project, _, err := client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() if err != nil { return ldapi.ClientSideAvailability{}, false, err } - project := rawProject.(ldapi.Project) + return *project.DefaultClientSideAvailability, project.IncludeInSnippetByDefault, nil } diff --git a/launchdarkly/helper.go b/launchdarkly/helper.go index 390583f5..c3855769 100644 --- a/launchdarkly/helper.go +++ b/launchdarkly/helper.go @@ -2,59 +2,13 @@ package launchdarkly import ( "fmt" - "log" "math/rand" "net/http" - "strconv" "time" ldapi "github.com/launchdarkly/api-client-go/v7" ) -const ( - MAX_409_RETRIES = 5 - MAX_429_RETRIES = 20 -) - -func handleRateLimit(apiCall func() (interface{}, *http.Response, error)) (interface{}, *http.Response, error) { - obj, res, err := apiCall() - for retryCount := 0; res != nil && res.StatusCode == http.StatusTooManyRequests && retryCount < MAX_429_RETRIES; retryCount++ { - log.Println("[DEBUG] received a 429 Too Many Requests error. retrying") - resetStr := res.Header.Get("X-RateLimit-Reset") - resetInt, parseErr := strconv.ParseInt(resetStr, 10, 64) - if parseErr != nil { - log.Println("[DEBUG] could not parse X-RateLimit-Reset header. Sleeping for a random interval.") - randomRetrySleep() - } else { - resetTime := time.Unix(0, resetInt*int64(time.Millisecond)) - sleepDuration := time.Until(resetTime) - - // We have observed situations where LD-s retry header results in a negative sleep duration. In this case, - // multiply the duration by -1 and add jitter - if sleepDuration <= 0 { - log.Printf("[DEBUG] received a negative rate limit retry duration of %s.", sleepDuration) - sleepDuration = -1 * sleepDuration - } - sleepDurationWithJitter := sleepDuration + getRandomSleepDuration(sleepDuration) - log.Println("[DEBUG] sleeping", sleepDurationWithJitter) - time.Sleep(sleepDurationWithJitter) - } - obj, res, err = apiCall() - } - return obj, res, err - -} - -func handleNoConflict(apiCall func() (interface{}, *http.Response, error)) (interface{}, *http.Response, error) { - obj, res, err := apiCall() - for retryCount := 0; res != nil && res.StatusCode == http.StatusConflict && retryCount < MAX_409_RETRIES; retryCount++ { - log.Println("[DEBUG] received a 409 conflict. retrying") - randomRetrySleep() - obj, res, err = apiCall() - } - return obj, res, err -} - var randomRetrySleepSeeded = false // getRandomSleepDuration returns a duration between [0, maxDuration) @@ -66,12 +20,6 @@ func getRandomSleepDuration(maxDuration time.Duration) time.Duration { return time.Duration(n) } -// Sleep for a random interval between 200ms and 500ms -func randomRetrySleep() { - duration := 200*time.Millisecond + getRandomSleepDuration(300*time.Millisecond) - time.Sleep(duration) -} - func ptr(v interface{}) *interface{} { return &v } func intPtr(i int) *int { diff --git a/launchdarkly/helper_test.go b/launchdarkly/helper_test.go deleted file mode 100644 index 6ba99704..00000000 --- a/launchdarkly/helper_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package launchdarkly - -import ( - "errors" - "net/http" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestHandleNoConflict(t *testing.T) { - t.Run("no retries needed", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleNoConflict(func() (interface{}, *http.Response, error) { - calls++ - return nil, &http.Response{StatusCode: http.StatusOK}, nil - }) - require.NoError(t, err) - assert.Equal(t, 1, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) - t.Run("max retries exceeded", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleNoConflict(func() (interface{}, *http.Response, error) { - calls++ - return nil, &http.Response{StatusCode: http.StatusConflict}, errors.New("Conflict") - }) - require.Error(t, err) - assert.Equal(t, 6, calls) - assert.Equal(t, res.StatusCode, http.StatusConflict) - }) - t.Run("conflict resolved", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleNoConflict(func() (interface{}, *http.Response, error) { - calls++ - if calls == 3 { - return nil, &http.Response{StatusCode: http.StatusOK}, nil - } - return nil, &http.Response{StatusCode: http.StatusConflict}, nil - }) - require.NoError(t, err) - assert.Equal(t, 3, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) -} - -func TestHandleRateLimit(t *testing.T) { - t.Run("no retries needed", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - calls++ - return nil, &http.Response{StatusCode: http.StatusOK}, nil - }) - require.NoError(t, err) - assert.Equal(t, 1, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) - t.Run("max retries exceeded", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - calls++ - res := &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}} - res.Header.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) - return nil, res, errors.New("Rate limit exceeded") - }) - require.Error(t, err) - assert.Equal(t, MAX_429_RETRIES+1, calls) - assert.Equal(t, res.StatusCode, http.StatusTooManyRequests) - }) - t.Run("retry resolved with header", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - calls++ - if calls == 3 { - return nil, &http.Response{StatusCode: http.StatusOK}, nil - } - res := &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}} - res.Header.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) - return nil, res, errors.New("Rate limit exceeded") - }) - require.NoError(t, err) - assert.Equal(t, 3, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) - t.Run("retry resolved with negative header", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - calls++ - if calls == 3 { - return nil, &http.Response{StatusCode: http.StatusOK}, nil - } - res := &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}} - res.Header.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(-100*time.Millisecond).UnixNano()/int64(time.Millisecond), 10)) - return nil, res, errors.New("Rate limit exceeded") - }) - require.NoError(t, err) - assert.Equal(t, 3, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) - t.Run("retry resolved without header", func(t *testing.T) { - t.Parallel() - calls := 0 - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - calls++ - if calls == 3 { - return nil, &http.Response{StatusCode: http.StatusOK}, nil - } - res := &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{}} - return nil, res, errors.New("Rate limit exceeded") - }) - require.NoError(t, err) - assert.Equal(t, 3, calls) - assert.Equal(t, res.StatusCode, http.StatusOK) - }) -} diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index abcbfb81..b86f5916 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -3,19 +3,16 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go/v7" ) func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) error { client := meta.(*Client) projectKey := d.Get(KEY).(string) - rawProject, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() - }) + project, res, err := client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() + // return nil error for resource reads but 404 for data source reads if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find project with key %q, removing from state if present", projectKey) @@ -26,7 +23,6 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er return fmt.Errorf("failed to get project with key %q: %v", projectKey, err) } - project := rawProject.(ldapi.Project) defaultCSA := *project.DefaultClientSideAvailability clientSideAvailability := []map[string]interface{}{{ "using_environment_id": defaultCSA.UsingEnvironmentId, diff --git a/launchdarkly/resource_launchdarkly_access_token.go b/launchdarkly/resource_launchdarkly_access_token.go index 34df7b7e..5bc80c75 100644 --- a/launchdarkly/resource_launchdarkly_access_token.go +++ b/launchdarkly/resource_launchdarkly_access_token.go @@ -156,10 +156,8 @@ func resourceAccessTokenCreate(d *schema.ResourceData, metaRaw interface{}) erro accessTokenBody.Role = ldapi.PtrString(accessTokenRole.(string)) } - tokenRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.PostToken(client.ctx).AccessTokenPost(accessTokenBody).Execute() - }) - token := tokenRaw.(ldapi.Token) + token, _, err := client.ld.AccessTokensApi.PostToken(client.ctx).AccessTokenPost(accessTokenBody).Execute() + if err != nil { return fmt.Errorf("failed to create access token with name %q: %s", accessTokenName, handleLdapiErr(err)) } @@ -173,10 +171,8 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error client := metaRaw.(*Client) accessTokenID := d.Id() - accessTokenRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.GetToken(client.ctx, accessTokenID).Execute() - }) - accessToken := accessTokenRaw.(ldapi.Token) + accessToken, res, err := client.ld.AccessTokensApi.GetToken(client.ctx, accessTokenID).Execute() + if isStatusNotFound(res) { log.Printf("[WARN] failed to find access token with id %q, removing from state", accessTokenID) d.SetId("") @@ -275,9 +271,7 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro patch = append(patch, op) } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccessTokensApi.PatchToken(client.ctx, accessTokenID).PatchOperation(patch).Execute() - }) + _, _, err = client.ld.AccessTokensApi.PatchToken(client.ctx, accessTokenID).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } @@ -304,10 +298,8 @@ func resourceAccessTokenDelete(d *schema.ResourceData, metaRaw interface{}) erro client := metaRaw.(*Client) accessTokenID := d.Id() - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.AccessTokensApi.DeleteToken(client.ctx, accessTokenID).Execute() - return nil, res, err - }) + _, err := client.ld.AccessTokensApi.DeleteToken(client.ctx, accessTokenID).Execute() + if err != nil { return fmt.Errorf("failed to delete access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } diff --git a/launchdarkly/resource_launchdarkly_custom_role.go b/launchdarkly/resource_launchdarkly_custom_role.go index 6447372b..e760f1fc 100644 --- a/launchdarkly/resource_launchdarkly_custom_role.go +++ b/launchdarkly/resource_launchdarkly_custom_role.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -67,9 +66,8 @@ func resourceCustomRoleCreate(d *schema.ResourceData, metaRaw interface{}) error Policy: customRolePolicies, } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.PostCustomRole(client.ctx).CustomRolePost(customRoleBody).Execute() - }) + _, _, err = client.ld.CustomRolesApi.PostCustomRole(client.ctx).CustomRolePost(customRoleBody).Execute() + if err != nil { return fmt.Errorf("failed to create custom role with name %q: %s", customRoleName, handleLdapiErr(err)) } @@ -82,10 +80,8 @@ func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { client := metaRaw.(*Client) customRoleID := d.Id() - customRoleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() - }) - customRole := customRoleRaw.(ldapi.CustomRole) + customRole, res, err := client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() + if isStatusNotFound(res) { log.Printf("[WARN] failed to find custom role with id %q, removing from state", customRoleID) d.SetId("") @@ -134,11 +130,7 @@ func resourceCustomRoleUpdate(d *schema.ResourceData, metaRaw interface{}) error patchReplace("/policy", &customRolePolicies), }} - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.PatchCustomRole(client.ctx, customRoleKey).PatchWithComment(patch).Execute() - }) - }) + _, _, err = client.ld.CustomRolesApi.PatchCustomRole(client.ctx, customRoleKey).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) } @@ -150,10 +142,7 @@ func resourceCustomRoleDelete(d *schema.ResourceData, metaRaw interface{}) error client := metaRaw.(*Client) customRoleKey := d.Id() - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.CustomRolesApi.DeleteCustomRole(client.ctx, customRoleKey).Execute() - return nil, res, err - }) + _, err := client.ld.CustomRolesApi.DeleteCustomRole(client.ctx, customRoleKey).Execute() if err != nil { return fmt.Errorf("failed to delete custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) diff --git a/launchdarkly/resource_launchdarkly_destination.go b/launchdarkly/resource_launchdarkly_destination.go index 2a8561eb..bea08b7a 100644 --- a/launchdarkly/resource_launchdarkly_destination.go +++ b/launchdarkly/resource_launchdarkly_destination.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -86,10 +85,7 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro On: &destinationOn, } - destinationRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.PostDestination(client.ctx, destinationProjKey, destinationEnvKey).DestinationPost(destinationBody).Execute() - }) - destination := destinationRaw.(ldapi.Destination) + destination, _, err := client.ld.DataExportDestinationsApi.PostDestination(client.ctx, destinationProjKey, destinationEnvKey).DestinationPost(destinationBody).Execute() if err != nil { d.SetId("") return fmt.Errorf("failed to create destination with project key %q and env key %q: %s", destinationProjKey, destinationEnvKey, handleLdapiErr(err)) @@ -111,10 +107,8 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) - destinationRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() - }) - destination := destinationRaw.(ldapi.Destination) + destination, res, err := client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() + if isStatusNotFound(res) { log.Printf("[WARN] failed to find destination with id: %q in project %q, environment: %q, removing from state", destinationID, destinationProjKey, destinationEnvKey) d.SetId("") @@ -159,11 +153,7 @@ func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) erro patchReplace("/config", &destinationConfig), } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict((func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.PatchDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).PatchOperation(patch).Execute() - })) - }) + _, _, err = client.ld.DataExportDestinationsApi.PatchDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update destination with id %q: %s", destinationID, handleLdapiErr(err)) } @@ -180,11 +170,7 @@ func resourceDestinationDelete(d *schema.ResourceData, metaRaw interface{}) erro destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.DataExportDestinationsApi.DeleteDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() - return nil, res, err - }) - + _, err = client.ld.DataExportDestinationsApi.DeleteDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() if err != nil { return fmt.Errorf("failed to delete destination with id %q: %s", destinationID, handleLdapiErr(err)) } @@ -201,9 +187,7 @@ func resourceDestinationExists(d *schema.ResourceData, metaRaw interface{}) (boo destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() - }) + _, res, err := client.ld.DataExportDestinationsApi.GetDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_environment.go b/launchdarkly/resource_launchdarkly_environment.go index 31665b33..961a794f 100644 --- a/launchdarkly/resource_launchdarkly_environment.go +++ b/launchdarkly/resource_launchdarkly_environment.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -58,9 +57,7 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro ConfirmChanges: &confirmChanges, } - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() - }) + _, _, err := client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() if err != nil { return fmt.Errorf("failed to create environment: [%+v] for project key: %s: %s", envPost, projectKey, handleLdapiErr(err)) } @@ -116,11 +113,7 @@ func resourceEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) erro return err } patch = append(patch, approvalPatch...) - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key).PatchOperation(patch).Execute() - }) - }) + _, _, err = client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update environment with key %q for project: %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -133,11 +126,7 @@ func resourceEnvironmentDelete(d *schema.ResourceData, metaRaw interface{}) erro projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() - return nil, res, err - }) - + _, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() if err != nil { return fmt.Errorf("failed to delete project with key %q for project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -150,9 +139,7 @@ func resourceEnvironmentExists(d *schema.ResourceData, metaRaw interface{}) (boo } func environmentExists(projectKey string, key string, meta *Client) (bool, error) { - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.EnvironmentsApi.GetEnvironment(meta.ctx, projectKey, key).Execute() - }) + _, res, err := meta.ld.EnvironmentsApi.GetEnvironment(meta.ctx, projectKey, key).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index be715bac..4ca1bd71 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -3,7 +3,6 @@ package launchdarkly import ( "context" "fmt" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -140,10 +139,7 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro UsingMobileKey: *defaultCSA.UsingMobileKey, } } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, projectKey).FeatureFlagBody(flag).Execute() - }) - + _, _, err = client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, projectKey).FeatureFlagBody(flag).Execute() if err != nil { return fmt.Errorf("failed to create flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -246,12 +242,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro patch.Patch = append(patch.Patch, patchReplace("/maintainerId", maintainerID.(string))) } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(*&patch).Execute() - }) - }) - + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(*&patch).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -264,10 +255,7 @@ func resourceFeatureFlagDelete(d *schema.ResourceData, metaRaw interface{}) erro projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() - return nil, res, err - }) + _, err := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() if err != nil { return fmt.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment.go b/launchdarkly/resource_launchdarkly_feature_flag_environment.go index c5a34fcf..823425c5 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -112,11 +111,7 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf } log.Printf("[DEBUG] %+v\n", patch) - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() - }) - }) + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q: %s", flagKey, projectKey, handleLdapiErr(err)) } @@ -182,11 +177,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf }} log.Printf("[DEBUG] %+v\n", patch) - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() - }) - }) + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } @@ -238,11 +229,7 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf }} log.Printf("[DEBUG] %+v\n", patch) - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() - }) - }) + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index ce08564f..d6105519 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -132,9 +131,7 @@ func resourceProjectCreate(d *schema.ResourceData, metaRaw interface{}) error { projectBody.Environments = &envs } - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() - }) + _, _, err := client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() if err != nil { return fmt.Errorf("failed to create project with name %s and projectKey %s: %v", name, projectKey, handleLdapiErr(err)) } @@ -190,24 +187,17 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { })) } - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PatchProject(client.ctx, projectKey).PatchOperation(patch).Execute() - }) - }) + _, _, err := client.ld.ProjectsApi.PatchProject(client.ctx, projectKey).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update project with key %q: %s", projectKey, handleLdapiErr(err)) } // Update environments if necessary oldSchemaEnvList, newSchemaEnvList := d.GetChange(ENVIRONMENTS) // Get the project so we can see if we need to create any environments or just update existing environments - rawProject, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() - }) + project, _, err := client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() if err != nil { return fmt.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) } - project := rawProject.(ldapi.Project) environmentConfigs := newSchemaEnvList.([]interface{}) oldEnvironmentConfigs := oldSchemaEnvList.([]interface{}) @@ -228,9 +218,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { exists := environmentExistsInProject(project, envKey) if !exists { envPost := environmentPostFromResourceData(env) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() - }) + _, _, err := client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() if err != nil { return fmt.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) } @@ -245,11 +233,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { if err != nil { return err } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey).PatchOperation(patch).Execute() - }) - }) + _, _, err = client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update environment with key %q for project: %q: %+v", envKey, projectKey, err) } @@ -261,10 +245,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { envConfig := env.(map[string]interface{}) envKey := envConfig[KEY].(string) if _, persists := envConfigsForCompare[envKey]; !persists { - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey).Execute() - return nil, res, err - }) + _, err = client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey).Execute() if err != nil { return fmt.Errorf("failed to delete environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) } @@ -278,11 +259,7 @@ func resourceProjectDelete(d *schema.ResourceData, metaRaw interface{}) error { client := metaRaw.(*Client) projectKey := d.Get(KEY).(string) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey).Execute() - return nil, res, err - }) - + _, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey).Execute() if err != nil { return fmt.Errorf("failed to delete project with key %q: %s", projectKey, handleLdapiErr(err)) } @@ -295,9 +272,7 @@ func resourceProjectExists(d *schema.ResourceData, metaRaw interface{}) (bool, e } func projectExists(projectKey string, meta *Client) (bool, error) { - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.ProjectsApi.GetProject(meta.ctx, projectKey).Execute() - }) + _, res, err := meta.ld.ProjectsApi.GetProject(meta.ctx, projectKey).Execute() if isStatusNotFound(res) { log.Println("got 404 when getting project. returning false.") return false, nil diff --git a/launchdarkly/resource_launchdarkly_segment.go b/launchdarkly/resource_launchdarkly_segment.go index d8a3bd2d..e69bec96 100644 --- a/launchdarkly/resource_launchdarkly_segment.go +++ b/launchdarkly/resource_launchdarkly_segment.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -69,10 +68,7 @@ func resourceSegmentCreate(d *schema.ResourceData, metaRaw interface{}) error { Tags: &tags, } - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.PostSegment(client.ctx, projectKey, envKey).SegmentBody(segment).Execute() - }) - + _, _, err := client.ld.SegmentsApi.PostSegment(client.ctx, projectKey, envKey).SegmentBody(segment).Execute() if err != nil { return fmt.Errorf("failed to create segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -120,11 +116,7 @@ func resourceSegmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { patchReplace("/rules", rules), }} - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, key).PatchWithComment(patch).Execute() - }) - }) + _, _, err = client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, key).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -138,11 +130,7 @@ func resourceSegmentDelete(d *schema.ResourceData, metaRaw interface{}) error { envKey := d.Get(ENV_KEY).(string) key := d.Get(KEY).(string) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.SegmentsApi.DeleteSegment(client.ctx, projectKey, envKey, key).Execute() - return nil, res, err - }) - + _, err := client.ld.SegmentsApi.DeleteSegment(client.ctx, projectKey, envKey, key).Execute() if err != nil { return fmt.Errorf("failed to delete segment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } @@ -156,9 +144,7 @@ func resourceSegmentExists(d *schema.ResourceData, metaRaw interface{}) (bool, e envKey := d.Get(ENV_KEY).(string) key := d.Get(KEY).(string) - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, key).Execute() - }) + _, res, err := client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, key).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/resource_launchdarkly_team_member.go b/launchdarkly/resource_launchdarkly_team_member.go index 9fd93b15..3b8e47f8 100644 --- a/launchdarkly/resource_launchdarkly_team_member.go +++ b/launchdarkly/resource_launchdarkly_team_member.go @@ -3,7 +3,6 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -81,10 +80,7 @@ func resourceTeamMemberCreate(d *schema.ResourceData, metaRaw interface{}) error CustomRoles: &customRoles, } - membersRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm([]ldapi.NewMemberForm{membersBody}).Execute() - }) - members := membersRaw.(ldapi.Members) + members, _, err := client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm([]ldapi.NewMemberForm{membersBody}).Execute() if err != nil { return fmt.Errorf("failed to create team member with email: %s: %v", memberEmail, handleLdapiErr(err)) } @@ -97,10 +93,7 @@ func resourceTeamMemberRead(d *schema.ResourceData, metaRaw interface{}) error { client := metaRaw.(*Client) memberID := d.Id() - memberRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Execute() - }) - member := memberRaw.(ldapi.Member) + member, res, err := client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Execute() if isStatusNotFound(res) { log.Printf("[WARN] failed to find member with id %q, removing from state", memberID) d.SetId("") @@ -148,11 +141,7 @@ func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error patchReplace("/customRoles", &customRoleIds), } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.AccountMembersApi.PatchMember(client.ctx, memberID).PatchOperation(patch).Execute() - }) - }) + _, _, err = client.ld.AccountMembersApi.PatchMember(client.ctx, memberID).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update team member with id %q: %s", memberID, handleLdapiErr(err)) } @@ -163,10 +152,7 @@ func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error func resourceTeamMemberDelete(d *schema.ResourceData, metaRaw interface{}) error { client := metaRaw.(*Client) - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.AccountMembersApi.DeleteMember(client.ctx, d.Id()).Execute() - return nil, res, err - }) + _, err := client.ld.AccountMembersApi.DeleteMember(client.ctx, d.Id()).Execute() if err != nil { return fmt.Errorf("failed to delete team member with id %q: %s", d.Id(), handleLdapiErr(err)) } diff --git a/launchdarkly/resource_launchdarkly_webhook.go b/launchdarkly/resource_launchdarkly_webhook.go index 06067fd3..909b2c82 100644 --- a/launchdarkly/resource_launchdarkly_webhook.go +++ b/launchdarkly/resource_launchdarkly_webhook.go @@ -2,7 +2,6 @@ package launchdarkly import ( "fmt" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -65,10 +64,7 @@ func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { webhookBody.Sign = true } - webhookRaw, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() - }) - webhook := webhookRaw.(ldapi.Webhook) + webhook, _, err := client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() if err != nil { return fmt.Errorf("failed to create webhook with name %q: %s", webhookName, handleLdapiErr(err)) } @@ -118,11 +114,7 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { } } - _, _, err = handleRateLimit(func() (interface{}, *http.Response, error) { - return handleNoConflict(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.PatchWebhook(client.ctx, webhookID).PatchOperation(patch).Execute() - }) - }) + _, _, err = client.ld.WebhooksApi.PatchWebhook(client.ctx, webhookID).PatchOperation(patch).Execute() if err != nil { return fmt.Errorf("failed to update webhook with id %q: %s", webhookID, handleLdapiErr(err)) } @@ -134,11 +126,7 @@ func resourceWebhookDelete(d *schema.ResourceData, metaRaw interface{}) error { client := metaRaw.(*Client) webhookID := d.Id() - _, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - res, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookID).Execute() - return nil, res, err - }) - + _, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookID).Execute() if err != nil { return fmt.Errorf("failed to delete webhook with id %q: %s", webhookID, handleLdapiErr(err)) } @@ -151,9 +139,7 @@ func resourceWebhookExists(d *schema.ResourceData, metaRaw interface{}) (bool, e } func webhookExists(webhookID string, meta *Client) (bool, error) { - _, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return meta.ld.WebhooksApi.GetWebhook(meta.ctx, webhookID).Execute() - }) + _, res, err := meta.ld.WebhooksApi.GetWebhook(meta.ctx, webhookID).Execute() if isStatusNotFound(res) { return false, nil } diff --git a/launchdarkly/segments_helper.go b/launchdarkly/segments_helper.go index 289aa89c..8d1d7f30 100644 --- a/launchdarkly/segments_helper.go +++ b/launchdarkly/segments_helper.go @@ -3,10 +3,8 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseSegmentSchema() map[string]*schema.Schema { @@ -44,10 +42,7 @@ func segmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) err envKey := d.Get(ENV_KEY).(string) segmentKey := d.Get(KEY).(string) - segmentRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, segmentKey).Execute() - }) - segment := segmentRaw.(ldapi.UserSegment) + segment, res, err := client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, segmentKey).Execute() if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find segment %q in project %q, environment %q, removing from state", segmentKey, projectKey, envKey) d.SetId("") diff --git a/launchdarkly/team_member_helper.go b/launchdarkly/team_member_helper.go index 45485c2d..e7349083 100644 --- a/launchdarkly/team_member_helper.go +++ b/launchdarkly/team_member_helper.go @@ -2,19 +2,13 @@ package launchdarkly import ( "fmt" - "net/http" - - ldapi "github.com/launchdarkly/api-client-go/v7" ) // The LD api returns custom role IDs (not keys). Since we want to set custom_roles with keys, we need to look up their IDs func customRoleIDsToKeys(client *Client, ids []string) ([]string, error) { customRoleKeys := make([]string, 0, len(ids)) for _, customRoleID := range ids { - roleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() - }) - role := roleRaw.(ldapi.CustomRole) + role, res, err := client.ld.CustomRolesApi.GetCustomRole(client.ctx, customRoleID).Execute() if isStatusNotFound(res) { return nil, fmt.Errorf("failed to find custom role key for ID %q", customRoleID) } @@ -30,10 +24,7 @@ func customRoleIDsToKeys(client *Client, ids []string) ([]string, error) { func customRoleKeysToIDs(client *Client, keys []string) ([]string, error) { customRoleIds := make([]string, 0, len(keys)) for _, key := range keys { - roleRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.CustomRolesApi.GetCustomRole(client.ctx, key).Execute() - }) - role := roleRaw.(ldapi.CustomRole) + role, res, err := client.ld.CustomRolesApi.GetCustomRole(client.ctx, key).Execute() if isStatusNotFound(res) { return nil, fmt.Errorf("failed to find custom ID for key %q", key) } diff --git a/launchdarkly/test_utils.go b/launchdarkly/test_utils.go index 38b6ca3b..6c4890d7 100644 --- a/launchdarkly/test_utils.go +++ b/launchdarkly/test_utils.go @@ -1,24 +1,16 @@ package launchdarkly import ( - "fmt" - "net/http" - ldapi "github.com/launchdarkly/api-client-go/v7" ) // testAccDataSourceProjectCreate creates a project with the given project parameters func testAccDataSourceProjectCreate(client *Client, projectBody ldapi.ProjectPost) (*ldapi.Project, error) { - project, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() - }) + project, _, err := client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() if err != nil { return nil, err } - if project, ok := project.(ldapi.Project); ok { - return &project, nil - } - return nil, fmt.Errorf("failed to create project") + return &project, nil } func testAccDataSourceProjectDelete(client *Client, projectKey string) error { @@ -39,16 +31,11 @@ func testAccDataSourceFeatureFlagScaffold(client *Client, projectKey string, fla return nil, err } - flag, _, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, project.Key).FeatureFlagBody(flagBody).Execute() - }) + flag, _, err := client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, project.Key).FeatureFlagBody(flagBody).Execute() if err != nil { return nil, err } - if flag, ok := flag.(ldapi.FeatureFlag); ok { - return &flag, nil - } - return nil, fmt.Errorf("failed to create flag") + return &flag, nil } diff --git a/launchdarkly/webhooks_helper.go b/launchdarkly/webhooks_helper.go index 02a28b07..692c63db 100644 --- a/launchdarkly/webhooks_helper.go +++ b/launchdarkly/webhooks_helper.go @@ -3,10 +3,8 @@ package launchdarkly import ( "fmt" "log" - "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - ldapi "github.com/launchdarkly/api-client-go/v7" ) func baseWebhookSchema() map[string]*schema.Schema { @@ -36,10 +34,7 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er webhookID = d.Id() } - webhookRaw, res, err := handleRateLimit(func() (interface{}, *http.Response, error) { - return client.ld.WebhooksApi.GetWebhook(client.ctx, webhookID).Execute() - }) - webhook := webhookRaw.(ldapi.Webhook) + webhook, res, err := client.ld.WebhooksApi.GetWebhook(client.ctx, webhookID).Execute() if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find webhook with id %q, removing from state", webhookID) d.SetId("") From bd2580cf48c170c3549c248586a0267bc0f14f4b Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Mon, 10 Jan 2022 14:02:42 +0000 Subject: [PATCH 21/36] Add golangci-lint pre-commit hook and run hooks in CI (#175) --- .circleci/config.yml | 25 ++++++++++++++----- .go-version | 1 + .pre-commit-config.yaml | 9 ++++++- CHANGELOG.md | 2 ++ GNUmakefile | 2 ++ launchdarkly/policy_statements_helper.go | 8 +----- .../resource_launchdarkly_feature_flag.go | 14 ++++++++--- ...resource_launchdarkly_feature_flag_test.go | 13 ---------- launchdarkly/resource_launchdarkly_project.go | 11 ++++++-- launchdarkly/validation_helper.go | 5 ++++ scripts/errcheck.sh | 2 +- scripts/gofmtcheck.sh | 16 +++++++++++- 12 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 .go-version diff --git a/.circleci/config.yml b/.circleci/config.yml index fafed610..f609e11a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,19 +3,17 @@ version: 2.1 orbs: go: circleci/go@1.7.0 + linter: talkiq/linter@1.4.1 jobs: - build: + test: executor: name: go/default - tag: "1.16" + tag: &go_version "1.16.10" steps: - checkout - go/mod-download-cached - - run: - name: go vet - command: make vet - run: name: Run unit tests command: TESTARGS="-v" make test @@ -57,7 +55,22 @@ jobs: name: Test Webhook Resource command: TESTARGS="-run TestAccWebhook" make testacc + lint: + executor: + name: go/default + tag: *go_version + + steps: + - checkout + - run: + name: Install python + command: | + sudo apt update + sudo apt install python3-pip python-is-python3 + - linter/pre-commit + workflows: main: jobs: - - build + - test + - lint diff --git a/.go-version b/.go-version new file mode 100644 index 00000000..d3799fb2 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.16.10 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85914761..caf53f4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,12 @@ +minimum_pre_commit_version: "2.9.3" + repos: - - repo: git@github.com:ashanbrown/gofmts + - repo: https://github.com/ashanbrown/gofmts rev: v0.1.4 hooks: - id: gofmts + + - repo: https://github.com/golangci/golangci-lint + rev: v1.43.0 + hooks: + - id: golangci-lint diff --git a/CHANGELOG.md b/CHANGELOG.md index d300693a..2e8506db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ENHANCEMENTS: - Added a pre-commit file with a hook to alphabetize launchdarkly/keys.go +- Improved 409 and 429 retry handling. + ## [2.3.0] (January 4, 2022) FEATURES: diff --git a/GNUmakefile b/GNUmakefile index f5bb61b4..c3ff6172 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -29,6 +29,8 @@ vet: fi fmt: + go install github.com/ashanbrown/gofmts/cmd/gofmts@v0.1.4 + gofmts -w $(GOFMT_FILES) gofmt -w $(GOFMT_FILES) fmtcheck: diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 36b6a50b..1a2401c6 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -167,13 +167,7 @@ func policyStatementsToResourceData(statements []ldapi.StatementRep) []interface func statementsToStatementReps(policies []ldapi.Statement) []ldapi.StatementRep { statements := make([]ldapi.StatementRep, 0, len(policies)) for _, p := range policies { - rep := ldapi.StatementRep{ - Resources: p.Resources, - Actions: p.Actions, - NotResources: p.NotResources, - NotActions: p.NotActions, - Effect: p.Effect, - } + rep := ldapi.StatementRep(p) statements = append(statements, rep) } return statements diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index 4ca1bd71..e30282bd 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -37,11 +37,17 @@ func customizeFlagDiff(ctx context.Context, diff *schema.ResourceDiff, v interfa // AND the customer removes the INCLUDE_IN_SNIPPET key from the config without replacing with defaultCSA // The read would assume no changes are needed, HOWEVER we need to jump back to project level set defaults // Hence the setting below - diff.SetNew(INCLUDE_IN_SNIPPET, includeInSnippetByDefault) - diff.SetNew(CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ + err := diff.SetNew(INCLUDE_IN_SNIPPET, includeInSnippetByDefault) + if err != nil { + return err + } + err = diff.SetNew(CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ USING_ENVIRONMENT_ID: defaultCSA.UsingEnvironmentId, USING_MOBILE_KEY: defaultCSA.UsingMobileKey, }}) + if err != nil { + return err + } } } @@ -91,6 +97,7 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro includeInSnippet := d.Get(INCLUDE_IN_SNIPPET).(bool) // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + //nolint:staticcheck // SA1019 _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) _, clientSideAvailabilityOk := d.GetOk(CLIENT_SIDE_AVAILABILITY) clientSideAvailability := &ldapi.ClientSideAvailabilityPost{ @@ -178,6 +185,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro clientSideHasChange := d.HasChange(CLIENT_SIDE_AVAILABILITY) // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + //nolint:staticcheck // SA1019 _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) _, clientSideAvailabilityOk := d.GetOk(CLIENT_SIDE_AVAILABILITY) temporary := d.Get(TEMPORARY).(bool) @@ -242,7 +250,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro patch.Patch = append(patch.Patch, patchReplace("/maintainerId", maintainerID.(string))) } - _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(*&patch).Execute() + _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(patch).Execute() if err != nil { return fmt.Errorf("failed to update flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } diff --git a/launchdarkly/resource_launchdarkly_feature_flag_test.go b/launchdarkly/resource_launchdarkly_feature_flag_test.go index e5d82a49..d0c124f0 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_test.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_test.go @@ -307,19 +307,6 @@ resource "launchdarkly_feature_flag" "defaults" { } } ` - testAccFeatureFlagDefaultsMissingOffInvalid = ` -resource "launchdarkly_feature_flag" "defaults" { - project_key = launchdarkly_project.test.key - key = "defaults-flag" - name = "Feature flag with defaults" - variation_type = "boolean" - defaults { - on_variation = 2 - off_variation = 3 - } -} -` - testAccFeatureFlagDefaultsMultivariate = ` resource "launchdarkly_feature_flag" "defaults-multivariate" { project_key = launchdarkly_project.test.key diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index d6105519..725d5c90 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -31,11 +31,17 @@ func customizeProjectDiff(ctx context.Context, diff *schema.ResourceDiff, v inte // AND the customer removes the INCLUDE_IN_SNIPPET key from the config without replacing with defaultCSA // The read would assume no changes are needed, HOWEVER we need to jump back to LD set defaults // Hence the setting below - diff.SetNew(INCLUDE_IN_SNIPPET, false) - diff.SetNew(CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ + err := diff.SetNew(INCLUDE_IN_SNIPPET, false) + if err != nil { + return err + } + err = diff.SetNew(DEFAULT_CLIENT_SIDE_AVAILABILITY, []map[string]interface{}{{ USING_ENVIRONMENT_ID: false, USING_MOBILE_KEY: true, }}) + if err != nil { + return err + } } @@ -159,6 +165,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { clientSideHasChange := d.HasChange(DEFAULT_CLIENT_SIDE_AVAILABILITY) // GetOkExists is 'deprecated', but needed as optional booleans set to false return a 'false' ok value from GetOk // Also not really deprecated as they are keeping it around pending a replacement https://github.com/hashicorp/terraform-plugin-sdk/pull/350#issuecomment-597888969 + //nolint:staticcheck // SA1019 _, includeInSnippetOk := d.GetOkExists(INCLUDE_IN_SNIPPET) _, clientSideAvailabilityOk := d.GetOk(DEFAULT_CLIENT_SIDE_AVAILABILITY) defaultClientSideAvailability := &ldapi.ClientSideAvailabilityPost{ diff --git a/launchdarkly/validation_helper.go b/launchdarkly/validation_helper.go index 9aca7cc9..928f0cbc 100644 --- a/launchdarkly/validation_helper.go +++ b/launchdarkly/validation_helper.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateKey() schema.SchemaValidateFunc { return validation.StringMatch( regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`), @@ -14,6 +15,7 @@ func validateKey() schema.SchemaValidateFunc { ) } +//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateKeyAndLength(minLength, maxLength int) schema.SchemaValidateFunc { return validation.All( validation.StringMatch( @@ -24,6 +26,7 @@ func validateKeyAndLength(minLength, maxLength int) schema.SchemaValidateFunc { ) } +//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateID() schema.SchemaValidateFunc { return validation.All( validation.StringMatch(regexp.MustCompile(`^[a-fA-F0-9]*$`), "Must be a 24 character hexadecimal string"), @@ -31,6 +34,7 @@ func validateID() schema.SchemaValidateFunc { ) } +//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateTags() schema.SchemaValidateFunc { return validation.All( validation.StringLenBetween(1, 64), @@ -41,6 +45,7 @@ func validateTags() schema.SchemaValidateFunc { ) } +//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateOp() schema.SchemaValidateFunc { return validation.StringInSlice([]string{ "in", diff --git a/scripts/errcheck.sh b/scripts/errcheck.sh index 15464f5a..76590d5c 100755 --- a/scripts/errcheck.sh +++ b/scripts/errcheck.sh @@ -5,7 +5,7 @@ echo "==> Checking for unchecked errors..." if ! which errcheck > /dev/null; then echo "==> Installing errcheck..." - go get -u github.com/kisielk/errcheck + go install github.com/kisielk/errcheck@v1.5.0 fi err_files=$(errcheck -ignoretests \ diff --git a/scripts/gofmtcheck.sh b/scripts/gofmtcheck.sh index 1c055815..dcaab03c 100755 --- a/scripts/gofmtcheck.sh +++ b/scripts/gofmtcheck.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash +echo "==> Checking that code complies with gofmt and gmts requirements..." # Check gofmt -echo "==> Checking that code complies with gofmt requirements..." gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) if [[ -n ${gofmt_files} ]]; then echo 'gofmt needs running on the following files:' @@ -10,4 +10,18 @@ if [[ -n ${gofmt_files} ]]; then exit 1 fi +# Check gofmts +if ! which gofmts > /dev/null; then + echo "==> Installing gofmts..." + go install github.com/ashanbrown/gofmts/cmd/gofmts@v0.1.4 +fi +gofmts_files=$(gofmts -l `find . -name '*.go' | grep -v vendor`) +if [[ -n ${gofmt_files} ]]; then + echo 'gofmts needs running on the following files:' + echo "${gofmts_files}" + echo "You can use the command: \`make fmt\` to reformat code." + exit 1 +fi + + exit 0 From 31bb4e966058e0fbaf5b0e607d07045664fe0c26 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 10 Jan 2022 16:02:11 +0000 Subject: [PATCH 22/36] Ffeldberg/sc 136452/use the context aware version of all crud (#174) * chore: refactor resource CRUD funcs to be ctx aware * feat: convert validation helpers to context aware funcs * feat: convert inline validation to diag aware func * chore: apply ToDiagFunc wrapper to other custom validators * fix: TypeList validators cant use ToDiagFunc * fix: TypeSett validators cant use ToDiagFunc --- launchdarkly/approvals_helper.go | 16 ++-- launchdarkly/clause_helper.go | 12 +-- launchdarkly/custom_properties_helper.go | 16 ++-- .../data_source_launchdarkly_environment.go | 21 +++-- .../data_source_launchdarkly_feature_flag.go | 10 ++- ...e_launchdarkly_feature_flag_environment.go | 15 ++-- .../data_source_launchdarkly_project.go | 9 +- .../data_source_launchdarkly_segment.go | 39 ++++---- .../data_source_launchdarkly_team_member.go | 13 +-- .../data_source_launchdarkly_webhook.go | 11 ++- launchdarkly/environments_helper.go | 25 ++++-- launchdarkly/fallthrough_helper.go | 8 +- .../feature_flag_environment_helper.go | 63 +++++++------ launchdarkly/feature_flags_helper.go | 74 ++++++++------- launchdarkly/policy_statements_helper.go | 6 +- launchdarkly/prerequisite_helper.go | 18 ++-- launchdarkly/project_helper.go | 25 ++++-- .../resource_launchdarkly_access_token.go | 90 ++++++++++--------- .../resource_launchdarkly_custom_role.go | 60 +++++++------ .../resource_launchdarkly_destination.go | 75 +++++++++------- .../resource_launchdarkly_environment.go | 58 ++++++------ .../resource_launchdarkly_feature_flag.go | 63 +++++++------ ...e_launchdarkly_feature_flag_environment.go | 76 ++++++++-------- launchdarkly/resource_launchdarkly_project.go | 56 +++++++----- launchdarkly/resource_launchdarkly_segment.go | 78 ++++++++-------- .../resource_launchdarkly_team_member.go | 64 +++++++------ launchdarkly/resource_launchdarkly_webhook.go | 50 ++++++----- launchdarkly/rollout_helper.go | 4 +- launchdarkly/rule_helper.go | 10 +-- launchdarkly/segment_rule_helper.go | 10 +-- launchdarkly/segments_helper.go | 25 ++++-- launchdarkly/tags_helper.go | 6 +- launchdarkly/target_helper.go | 8 +- launchdarkly/validation_helper.go | 41 ++++++--- launchdarkly/variations_helper.go | 11 +-- launchdarkly/webhooks_helper.go | 14 +-- 36 files changed, 677 insertions(+), 503 deletions(-) diff --git a/launchdarkly/approvals_helper.go b/launchdarkly/approvals_helper.go index 42197ada..e3b8e4b1 100644 --- a/launchdarkly/approvals_helper.go +++ b/launchdarkly/approvals_helper.go @@ -28,11 +28,11 @@ func approvalSchema() *schema.Schema { Default: false, }, MIN_NUM_APPROVALS: { - Type: schema.TypeInt, - Optional: true, - Description: "The number of approvals required before an approval request can be applied.", - ValidateFunc: validation.IntBetween(1, 5), - Default: 1, + Type: schema.TypeInt, + Optional: true, + Description: "The number of approvals required before an approval request can be applied.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 5)), + Default: 1, }, CAN_APPLY_DECLINED_CHANGES: { Type: schema.TypeBool, @@ -45,8 +45,10 @@ func approvalSchema() *schema.Schema { Optional: true, Description: "An array of tags used to specify which flags with those tags require approval. You may only set requiredApprovalTags or required, not both.", Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validateTags(), + Type: schema.TypeString, + // Can't use validation.ToDiagFunc converted validators on TypeList at the moment + // https://github.com/hashicorp/terraform-plugin-sdk/issues/734 + ValidateFunc: validateTagsNoDiag(), }, }, }, diff --git a/launchdarkly/clause_helper.go b/launchdarkly/clause_helper.go index a128d2b3..168a538f 100644 --- a/launchdarkly/clause_helper.go +++ b/launchdarkly/clause_helper.go @@ -29,10 +29,10 @@ func clauseSchema() *schema.Schema { Description: "The user attribute to operate on", }, OP: { - Type: schema.TypeString, - Required: true, - Description: "The operator associated with the rule clause. Available options are in, endsWith, startsWith, matches, contains, lessThan, lessThanOrEqual, greaterThanOrEqual, before, after, segmentMatch, semVerEqual, semVerLessThan, and semVerGreaterThan", - ValidateFunc: validateOp(), + Type: schema.TypeString, + Required: true, + Description: "The operator associated with the rule clause. Available options are in, endsWith, startsWith, matches, contains, lessThan, lessThanOrEqual, greaterThanOrEqual, before, after, segmentMatch, semVerEqual, semVerLessThan, and semVerGreaterThan", + ValidateDiagFunc: validateOp(), }, VALUES: { Type: schema.TypeList, @@ -47,14 +47,14 @@ func clauseSchema() *schema.Schema { Default: STRING_CLAUSE_VALUE, Optional: true, Description: "The type for each of the clause's values. Available types are boolean, string, and number. If omitted, value_type defaults to string", - ValidateFunc: validation.StringInSlice( + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice( []string{ BOOL_CLAUSE_VALUE, STRING_CLAUSE_VALUE, NUMBER_CLAUSE_VALUE, }, false, - ), + )), }, NEGATE: { Type: schema.TypeBool, diff --git a/launchdarkly/custom_properties_helper.go b/launchdarkly/custom_properties_helper.go index 1abbc32f..75bc4c2d 100644 --- a/launchdarkly/custom_properties_helper.go +++ b/launchdarkly/custom_properties_helper.go @@ -22,21 +22,23 @@ func customPropertiesSchema() *schema.Schema { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ KEY: { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringLenBetween(1, CUSTOM_PROPERTY_CHAR_LIMIT), + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, CUSTOM_PROPERTY_CHAR_LIMIT)), }, NAME: { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringLenBetween(1, CUSTOM_PROPERTY_CHAR_LIMIT), + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, CUSTOM_PROPERTY_CHAR_LIMIT)), }, VALUE: { Type: schema.TypeList, Required: true, MaxItems: CUSTOM_PROPERTY_ITEM_LIMIT, Elem: &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, + // Can't use validation.ToDiagFunc converted validators on TypeList at the moment + // https://github.com/hashicorp/terraform-plugin-sdk/issues/734 ValidateFunc: validation.StringLenBetween(1, CUSTOM_PROPERTY_CHAR_LIMIT), }, }, diff --git a/launchdarkly/data_source_launchdarkly_environment.go b/launchdarkly/data_source_launchdarkly_environment.go index ae9d88bd..18ade538 100644 --- a/launchdarkly/data_source_launchdarkly_environment.go +++ b/launchdarkly/data_source_launchdarkly_environment.go @@ -1,20 +1,25 @@ package launchdarkly -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) func dataSourceEnvironment() *schema.Resource { envSchema := dataSourceEnvironmentSchema(false) envSchema[PROJECT_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateKey(), } return &schema.Resource{ - Read: dataSourceEnvironmentRead, - Schema: envSchema, + ReadContext: dataSourceEnvironmentRead, + Schema: envSchema, } } -func dataSourceEnvironmentRead(d *schema.ResourceData, meta interface{}) error { - return environmentRead(d, meta, true) +func dataSourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return environmentRead(ctx, d, meta, true) } diff --git a/launchdarkly/data_source_launchdarkly_feature_flag.go b/launchdarkly/data_source_launchdarkly_feature_flag.go index 66c4bac2..764b135d 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag.go @@ -1,8 +1,10 @@ package launchdarkly import ( + "context" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -20,11 +22,11 @@ func dataSourceFeatureFlag() *schema.Resource { BOOL_VARIATION, STRING_VARIATION, NUMBER_VARIATION, JSON_VARIATION), } return &schema.Resource{ - Read: dataSourceFeatureFlagRead, - Schema: schemaMap, + ReadContext: dataSourceFeatureFlagRead, + Schema: schemaMap, } } -func dataSourceFeatureFlagRead(d *schema.ResourceData, raw interface{}) error { - return featureFlagRead(d, raw, true) +func dataSourceFeatureFlagRead(ctx context.Context, d *schema.ResourceData, raw interface{}) diag.Diagnostics { + return featureFlagRead(ctx, d, raw, true) } diff --git a/launchdarkly/data_source_launchdarkly_feature_flag_environment.go b/launchdarkly/data_source_launchdarkly_feature_flag_environment.go index d019c924..521aea09 100644 --- a/launchdarkly/data_source_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/data_source_launchdarkly_feature_flag_environment.go @@ -1,14 +1,19 @@ package launchdarkly -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) func dataSourceFeatureFlagEnvironment() *schema.Resource { return &schema.Resource{ - Read: dataSourceFeatureFlagEnvironmentRead, - Schema: baseFeatureFlagEnvironmentSchema(true), + ReadContext: dataSourceFeatureFlagEnvironmentRead, + Schema: baseFeatureFlagEnvironmentSchema(true), } } -func dataSourceFeatureFlagEnvironmentRead(d *schema.ResourceData, meta interface{}) error { - return featureFlagEnvironmentRead(d, meta, true) +func dataSourceFeatureFlagEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return featureFlagEnvironmentRead(ctx, d, meta, true) } diff --git a/launchdarkly/data_source_launchdarkly_project.go b/launchdarkly/data_source_launchdarkly_project.go index 3d410b4b..d1d8f24e 100644 --- a/launchdarkly/data_source_launchdarkly_project.go +++ b/launchdarkly/data_source_launchdarkly_project.go @@ -1,12 +1,15 @@ package launchdarkly import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceProject() *schema.Resource { return &schema.Resource{ - Read: dataSourceProjectRead, + ReadContext: dataSourceProjectRead, Schema: map[string]*schema.Schema{ KEY: { @@ -55,6 +58,6 @@ func dataSourceProject() *schema.Resource { } } -func dataSourceProjectRead(d *schema.ResourceData, meta interface{}) error { - return projectRead(d, meta, true) +func dataSourceProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return projectRead(ctx, d, meta, true) } diff --git a/launchdarkly/data_source_launchdarkly_segment.go b/launchdarkly/data_source_launchdarkly_segment.go index 90b594ef..c80562d2 100644 --- a/launchdarkly/data_source_launchdarkly_segment.go +++ b/launchdarkly/data_source_launchdarkly_segment.go @@ -1,26 +1,31 @@ package launchdarkly -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) func dataSourceSegment() *schema.Resource { schemaMap := baseSegmentSchema() schemaMap[PROJECT_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ValidateFunc: validateKey(), - Description: "The segment's project key.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateKey(), + Description: "The segment's project key.", } schemaMap[ENV_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ValidateFunc: validateKey(), - Description: "The segment's environment key.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateKey(), + Description: "The segment's environment key.", } schemaMap[KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ValidateFunc: validateKey(), - Description: "The unique key that references the segment.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateKey(), + Description: "The unique key that references the segment.", } schemaMap[NAME] = &schema.Schema{ Type: schema.TypeString, @@ -28,11 +33,11 @@ func dataSourceSegment() *schema.Resource { Description: "The human-friendly name for the segment.", } return &schema.Resource{ - Read: dataSourceSegmentRead, - Schema: schemaMap, + ReadContext: dataSourceSegmentRead, + Schema: schemaMap, } } -func dataSourceSegmentRead(d *schema.ResourceData, raw interface{}) error { - return segmentRead(d, raw, true) +func dataSourceSegmentRead(ctx context.Context, d *schema.ResourceData, raw interface{}) diag.Diagnostics { + return segmentRead(ctx, d, raw, true) } diff --git a/launchdarkly/data_source_launchdarkly_team_member.go b/launchdarkly/data_source_launchdarkly_team_member.go index 9850e17d..2aeef7a9 100644 --- a/launchdarkly/data_source_launchdarkly_team_member.go +++ b/launchdarkly/data_source_launchdarkly_team_member.go @@ -1,15 +1,17 @@ package launchdarkly import ( + "context" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" ) func dataSourceTeamMember() *schema.Resource { return &schema.Resource{ - Read: dataSourceTeamMemberRead, + ReadContext: dataSourceTeamMemberRead, Schema: map[string]*schema.Schema{ EMAIL: { @@ -73,12 +75,13 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er } -func dataSourceTeamMemberRead(d *schema.ResourceData, meta interface{}) error { +func dataSourceTeamMemberRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics client := meta.(*Client) memberEmail := d.Get(EMAIL).(string) member, err := getTeamMemberByEmail(client, memberEmail) if err != nil { - return err + return diag.FromErr(err) } d.SetId(member.Id) _ = d.Set(EMAIL, member.Email) @@ -87,8 +90,8 @@ func dataSourceTeamMemberRead(d *schema.ResourceData, meta interface{}) error { _ = d.Set(ROLE, member.Role) err = d.Set(CUSTOM_ROLES, member.CustomRoles) if err != nil { - return fmt.Errorf("failed to set custom roles on team member with email %q: %v", member.Email, err) + return diag.Errorf("failed to set custom roles on team member with email %q: %v", member.Email, err) } - return nil + return diags } diff --git a/launchdarkly/data_source_launchdarkly_webhook.go b/launchdarkly/data_source_launchdarkly_webhook.go index 5472071e..91e13b7e 100644 --- a/launchdarkly/data_source_launchdarkly_webhook.go +++ b/launchdarkly/data_source_launchdarkly_webhook.go @@ -1,6 +1,9 @@ package launchdarkly import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -20,11 +23,11 @@ func dataSourceWebhook() *schema.Resource { Description: "The ID of the webhook", } return &schema.Resource{ - Read: dataSourceWebhookRead, - Schema: schemaMap, + ReadContext: dataSourceWebhookRead, + Schema: schemaMap, } } -func dataSourceWebhookRead(d *schema.ResourceData, meta interface{}) error { - return webhookRead(d, meta, true) +func dataSourceWebhookRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return webhookRead(ctx, d, meta, true) } diff --git a/launchdarkly/environments_helper.go b/launchdarkly/environments_helper.go index d6449138..1425cf49 100644 --- a/launchdarkly/environments_helper.go +++ b/launchdarkly/environments_helper.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -19,8 +21,8 @@ func baseEnvironmentSchema(forProject bool) map[string]*schema.Schema { Required: true, Description: "A project-unique key for the new environment", // Don't force new if the environment schema will be nested in a project - ForceNew: !forProject, - ValidateFunc: validateKey(), + ForceNew: !forProject, + ValidateDiagFunc: validateKey(), }, API_KEY: { Type: schema.TypeString, @@ -42,8 +44,8 @@ func baseEnvironmentSchema(forProject bool) map[string]*schema.Schema { Optional: true, Default: 0, // Default TTL should be between 0 and 60 minutes: https://docs.launchdarkly.com/docs/environments - Description: "The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK", - ValidateFunc: validation.IntBetween(0, 60), + Description: "The TTL for the environment. This must be between 0 and 60 minutes. The TTL setting only applies to environments using the PHP SDK", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 60)), }, SECURE_MODE: { Default: false, @@ -221,7 +223,8 @@ func rawEnvironmentConfigsToKeyList(rawEnvs []interface{}) []string { return keys } -func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool) error { +func environmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := meta.(*Client) projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) @@ -230,11 +233,15 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find environment with key %q in project %q, removing from state", key, projectKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find environment with key %q in project %q, removing from state", key, projectKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get environment with key %q for project key: %q: %v", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to get environment with key %q for project key: %q: %v", key, projectKey, handleLdapiErr(err)) } d.SetId(projectKey + "/" + key) @@ -254,9 +261,9 @@ func environmentRead(d *schema.ResourceData, meta interface{}, isDataSource bool if env.ApprovalSettings != nil { err = d.Set(APPROVAL_SETTINGS, approvalSettingsToResourceData(*env.ApprovalSettings)) if err != nil { - return err + return diag.FromErr(err) } } - return nil + return diags } diff --git a/launchdarkly/fallthrough_helper.go b/launchdarkly/fallthrough_helper.go index 7b866a7c..d7ad044a 100644 --- a/launchdarkly/fallthrough_helper.go +++ b/launchdarkly/fallthrough_helper.go @@ -25,10 +25,10 @@ func fallthroughSchema(forDataSource bool) *schema.Schema { Description: "Group percentage rollout by a custom attribute. This argument is only valid if rollout_weights is also specified", }, VARIATION: { - Type: schema.TypeInt, - Optional: true, - Description: "The integer variation index to serve in case of fallthrough", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Optional: true, + Description: "The integer variation index to serve in case of fallthrough", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, diff --git a/launchdarkly/feature_flag_environment_helper.go b/launchdarkly/feature_flag_environment_helper.go index b228a623..42913200 100644 --- a/launchdarkly/feature_flag_environment_helper.go +++ b/launchdarkly/feature_flag_environment_helper.go @@ -1,11 +1,13 @@ package launchdarkly import ( + "context" "fmt" "log" "net/http" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -14,18 +16,18 @@ import ( func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Schema { return map[string]*schema.Schema{ FLAG_ID: { - Type: schema.TypeString, - Required: true, - Description: "The global feature flag's unique id in the format `/`", - ForceNew: true, - ValidateFunc: validateFlagID, + Type: schema.TypeString, + Required: true, + Description: "The global feature flag's unique id in the format `/`", + ForceNew: true, + ValidateDiagFunc: validation.ToDiagFunc(validateFlagID), }, ENV_KEY: { - Type: schema.TypeString, - Required: true, - Description: "The LaunchDarkly environment key", - ForceNew: true, - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + Description: "The LaunchDarkly environment key", + ForceNew: true, + ValidateDiagFunc: validateKey(), }, ON: { Type: schema.TypeBool, @@ -44,11 +46,11 @@ func baseFeatureFlagEnvironmentSchema(forDataSource bool) map[string]*schema.Sch Default: false, }, OFF_VARIATION: { - Type: schema.TypeInt, - Required: !forDataSource, - Optional: forDataSource, - Description: "The index of the variation to serve if targeting is disabled", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Required: !forDataSource, + Optional: forDataSource, + Description: "The index of the variation to serve if targeting is disabled", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, } } @@ -58,31 +60,40 @@ func getFeatureFlagEnvironment(client *Client, projectKey, flagKey, environmentK return client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Env(environmentKey).Execute() } -func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) error { +func featureFlagEnvironmentRead(ctx context.Context, d *schema.ResourceData, raw interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := raw.(*Client) flagId := d.Get(FLAG_ID).(string) projectKey, flagKey, err := flagIdToKeys(flagId) if err != nil { - return err + return diag.FromErr(err) } envKey := d.Get(ENV_KEY).(string) flag, res, err := getFeatureFlagEnvironment(client, projectKey, flagKey, envKey) if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find flag %q in project %q, removing from state", flagKey, projectKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find flag %q in project %q, removing from state", flagKey, projectKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get flag %q of project %q: %s", flagKey, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to get flag %q of project %q: %s", flagKey, projectKey, handleLdapiErr(err)) } 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{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find environment %q for flag %q, removing from state", envKey, flagKey), + }) d.SetId("") - return nil + return diags } if isDataSource { @@ -97,29 +108,29 @@ func featureFlagEnvironmentRead(d *schema.ResourceData, raw interface{}, isDataS rules, err := rulesToResourceData(environment.Rules) if err != nil { - return fmt.Errorf("failed to read rules on flag with key %q: %v", flagKey, err) + return diag.Errorf("failed to read rules on flag with key %q: %v", flagKey, err) } err = d.Set(RULES, rules) if err != nil { - return fmt.Errorf("failed to set rules on flag with key %q: %v", flagKey, err) + return diag.Errorf("failed to set rules on flag with key %q: %v", flagKey, err) } err = d.Set(TARGETS, targetsToResourceData(environment.Targets)) if err != nil { - return fmt.Errorf("failed to set targets on flag with key %q: %v", flagKey, err) + return diag.Errorf("failed to set targets on flag with key %q: %v", flagKey, err) } err = d.Set(FALLTHROUGH, fallthroughToResourceData(environment.Fallthrough)) if err != nil { - return fmt.Errorf("failed to set flag fallthrough on flag with key %q: %v", flagKey, err) + return diag.Errorf("failed to set flag fallthrough on flag with key %q: %v", flagKey, err) } err = d.Set(OFF_VARIATION, environment.OffVariation) if err != nil { - return fmt.Errorf("failed to set off_variation on flag with key %q: %v", flagKey, err) + return diag.Errorf("failed to set off_variation on flag with key %q: %v", flagKey, err) } - return nil + return diags } func patchFlagEnvPath(d *schema.ResourceData, op string) string { diff --git a/launchdarkly/feature_flags_helper.go b/launchdarkly/feature_flags_helper.go index 582b6a67..8f170630 100644 --- a/launchdarkly/feature_flags_helper.go +++ b/launchdarkly/feature_flags_helper.go @@ -1,10 +1,12 @@ package launchdarkly import ( + "context" "fmt" "log" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -13,25 +15,25 @@ import ( func baseFeatureFlagSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ PROJECT_KEY: { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The LaunchDarkly project key", - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The LaunchDarkly project key", + ValidateDiagFunc: validateKey(), }, KEY: { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateKey(), - Description: "A unique key that will be used to reference the flag in your code", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateKey(), + Description: "A unique key that will be used to reference the flag in your code", }, MAINTAINER_ID: { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "The LaunchDarkly id of the user who will maintain the flag. If not set, the API will automatically apply the member associated with your Terraform API key or the most recently set maintainer", - ValidateFunc: validateID(), + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The LaunchDarkly id of the user who will maintain the flag. If not set, the API will automatically apply the member associated with your Terraform API key or the most recently set maintainer", + ValidateDiagFunc: validateID(), }, DESCRIPTION: { Type: schema.TypeString, @@ -85,16 +87,16 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ ON_VARIATION: { - Type: schema.TypeInt, - Required: true, - Description: "The index of the variation served when the flag is on for new environments", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Required: true, + Description: "The index of the variation served when the flag is on for new environments", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, OFF_VARIATION: { - Type: schema.TypeInt, - Required: true, - Description: "The index of the variation served when the flag is off for new environments", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Required: true, + Description: "The index of the variation served when the flag is off for new environments", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, @@ -108,7 +110,8 @@ func baseFeatureFlagSchema() map[string]*schema.Schema { } } -func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) error { +func featureFlagRead(ctx context.Context, d *schema.ResourceData, raw interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := raw.(*Client) projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) @@ -116,13 +119,18 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) flag, res, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, key).Execute() if isStatusNotFound(res) && !isDataSource { + // TODO: Can probably get rid of all of these WARN logs? log.Printf("[WARN] feature flag %q in project %q not found, removing from state", key, projectKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] feature flag %q in project %q not found, removing from state", key, projectKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get flag %q of project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to get flag %q of project %q: %s", key, projectKey, handleLdapiErr(err)) } transformedCustomProperties := customPropertiesToResourceData(flag.CustomProperties) @@ -149,30 +157,30 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) variationType, err := variationsToVariationType(flag.Variations) if err != nil { - return fmt.Errorf("failed to determine variation type on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to determine variation type on flag with key %q: %v", flag.Key, err) } err = d.Set(VARIATION_TYPE, variationType) if err != nil { - return fmt.Errorf("failed to set variation type on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to set variation type on flag with key %q: %v", flag.Key, err) } parsedVariations, err := variationsToResourceData(flag.Variations, variationType) if err != nil { - return fmt.Errorf("failed to parse variations on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to parse variations on flag with key %q: %v", flag.Key, err) } err = d.Set(VARIATIONS, parsedVariations) if err != nil { - return fmt.Errorf("failed to set variations on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to set variations on flag with key %q: %v", flag.Key, err) } err = d.Set(TAGS, flag.Tags) if err != nil { - return fmt.Errorf("failed to set tags on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to set tags on flag with key %q: %v", flag.Key, err) } err = d.Set(CUSTOM_PROPERTIES, transformedCustomProperties) if err != nil { - return fmt.Errorf("failed to set custom properties on flag with key %q: %v", flag.Key, err) + return diag.Errorf("failed to set custom properties on flag with key %q: %v", flag.Key, err) } var defaults []map[string]interface{} @@ -190,7 +198,7 @@ func featureFlagRead(d *schema.ResourceData, raw interface{}, isDataSource bool) _ = d.Set(DEFAULTS, defaults) d.SetId(projectKey + "/" + key) - return nil + return diags } func flagIdToKeys(id string) (projectKey string, flagKey string, err error) { diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 1a2401c6..4424211d 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -64,9 +64,9 @@ func policyStatementsSchema(options policyStatementSchemaOptions) *schema.Schema MinItems: 1, }, EFFECT: { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{"allow", "deny"}, false), + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"allow", "deny"}, false)), }, }, }, diff --git a/launchdarkly/prerequisite_helper.go b/launchdarkly/prerequisite_helper.go index 682da0a0..f4a71079 100644 --- a/launchdarkly/prerequisite_helper.go +++ b/launchdarkly/prerequisite_helper.go @@ -16,17 +16,17 @@ func prerequisitesSchema() *schema.Schema { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ FLAG_KEY: { - Type: schema.TypeString, - Required: true, - Description: "The prerequisite feature flag's key", - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + Description: "The prerequisite feature flag's key", + ValidateDiagFunc: validateKey(), }, VARIATION: { - Type: schema.TypeInt, - Elem: &schema.Schema{Type: schema.TypeInt}, - Required: true, - Description: "The index of the prerequisite feature flag's variation to target", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Elem: &schema.Schema{Type: schema.TypeInt}, + Required: true, + Description: "The index of the prerequisite feature flag's variation to target", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, diff --git a/launchdarkly/project_helper.go b/launchdarkly/project_helper.go index b86f5916..2a84d79f 100644 --- a/launchdarkly/project_helper.go +++ b/launchdarkly/project_helper.go @@ -1,13 +1,16 @@ package launchdarkly import ( + "context" "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) error { +func projectRead(ctx context.Context, d *schema.ResourceData, meta interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := meta.(*Client) projectKey := d.Get(KEY).(string) @@ -16,11 +19,15 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er // return nil error for resource reads but 404 for data source reads if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find project with key %q, removing from state if present", projectKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find project with key %q, removing from state if present", projectKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get project with key %q: %v", projectKey, err) + return diag.Errorf("failed to get project with key %q: %v", projectKey, err) } defaultCSA := *project.DefaultClientSideAvailability @@ -33,7 +40,7 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er d.SetId(project.Id) err = d.Set(CLIENT_SIDE_AVAILABILITY, clientSideAvailability) if err != nil { - return fmt.Errorf("could not set client_side_availability on project with key %q: %v", project.Key, err) + return diag.Errorf("could not set client_side_availability on project with key %q: %v", project.Key, err) } } _ = d.Set(KEY, project.Key) @@ -71,24 +78,24 @@ func projectRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er err = d.Set(ENVIRONMENTS, environments) if err != nil { - return fmt.Errorf("could not set environments on project with key %q: %v", project.Key, err) + return diag.Errorf("could not set environments on project with key %q: %v", project.Key, err) } err = d.Set(INCLUDE_IN_SNIPPET, project.IncludeInSnippetByDefault) if err != nil { - return fmt.Errorf("could not set include_in_snippet on project with key %q: %v", project.Key, err) + return diag.Errorf("could not set include_in_snippet on project with key %q: %v", project.Key, err) } } err = d.Set(TAGS, project.Tags) if err != nil { - return fmt.Errorf("could not set tags on project with key %q: %v", project.Key, err) + return diag.Errorf("could not set tags on project with key %q: %v", project.Key, err) } err = d.Set(DEFAULT_CLIENT_SIDE_AVAILABILITY, clientSideAvailability) if err != nil { - return fmt.Errorf("could not set default_client_side_availability on project with key %q: %v", project.Key, err) + return diag.Errorf("could not set default_client_side_availability on project with key %q: %v", project.Key, err) } - return nil + return diags } diff --git a/launchdarkly/resource_launchdarkly_access_token.go b/launchdarkly/resource_launchdarkly_access_token.go index 5bc80c75..7c7feb82 100644 --- a/launchdarkly/resource_launchdarkly_access_token.go +++ b/launchdarkly/resource_launchdarkly_access_token.go @@ -2,6 +2,7 @@ package launchdarkly import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -9,6 +10,7 @@ import ( "net/http" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -27,11 +29,11 @@ func resourceAccessToken() *schema.Resource { conflictsWith: []string{ROLE, CUSTOM_ROLES, INLINE_ROLES}, }) return &schema.Resource{ - Create: resourceAccessTokenCreate, - Read: resourceAccessTokenRead, - Update: resourceAccessTokenUpdate, - Delete: resourceAccessTokenDelete, - Exists: resourceAccessTokenExists, + CreateContext: resourceAccessTokenCreate, + ReadContext: resourceAccessTokenRead, + UpdateContext: resourceAccessTokenUpdate, + DeleteContext: resourceAccessTokenDelete, + Exists: resourceAccessTokenExists, Schema: map[string]*schema.Schema{ NAME: { @@ -40,11 +42,11 @@ func resourceAccessToken() *schema.Resource { Optional: true, }, ROLE: { - Type: schema.TypeString, - Description: `The default built-in role for the token. Available options are "reader", "writer", and "admin"`, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{"reader", "writer", "admin"}, false), - ConflictsWith: []string{CUSTOM_ROLES, POLICY_STATEMENTS}, + Type: schema.TypeString, + Description: `The default built-in role for the token. Available options are "reader", "writer", and "admin"`, + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"reader", "writer", "admin"}, false)), + ConflictsWith: []string{CUSTOM_ROLES, POLICY_STATEMENTS}, }, CUSTOM_ROLES: { Type: schema.TypeSet, @@ -64,12 +66,12 @@ func resourceAccessToken() *schema.Resource { Default: false, }, DEFAULT_API_VERSION: { - Type: schema.TypeInt, - Description: "The default API version for this token", - Optional: true, - ForceNew: true, - Computed: true, - ValidateFunc: validateAPIVersion, + Type: schema.TypeInt, + Description: "The default API version for this token", + Optional: true, + ForceNew: true, + Computed: true, + ValidateDiagFunc: validation.ToDiagFunc(validateAPIVersion), }, TOKEN: { Type: schema.TypeString, @@ -78,11 +80,11 @@ func resourceAccessToken() *schema.Resource { Sensitive: true, }, EXPIRE: { - Deprecated: "'expire' is deprecated and will be removed in the next major release of the LaunchDarkly provider", - Type: schema.TypeInt, - Description: "Replace the computed token secret with a new value. The expired secret will no longer be able to authorize usage of the LaunchDarkly API. Should be an expiration time for the current token secret, expressed as a Unix epoch time in milliseconds. Setting this to a negative value will expire the existing token immediately. To reset the token value again, change 'expire' to a new value. Setting this field at resource creation time WILL NOT set an expiration time for the token.", - Optional: true, - ValidateFunc: validation.NoZeroValues, + Deprecated: "'expire' is deprecated and will be removed in the next major release of the LaunchDarkly provider", + Type: schema.TypeInt, + Description: "Replace the computed token secret with a new value. The expired secret will no longer be able to authorize usage of the LaunchDarkly API. Should be an expiration time for the current token secret, expressed as a Unix epoch time in milliseconds. Setting this to a negative value will expire the existing token immediately. To reset the token value again, change 'expire' to a new value. Setting this field at resource creation time WILL NOT set an expiration time for the token.", + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.NoZeroValues), }, }, } @@ -119,10 +121,10 @@ func validateAccessTokenResource(d *schema.ResourceData) error { return nil } -func resourceAccessTokenCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceAccessTokenCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { err := validateAccessTokenResource(d) if err != nil { - return err + return diag.FromErr(err) } client := metaRaw.(*Client) @@ -159,15 +161,17 @@ func resourceAccessTokenCreate(d *schema.ResourceData, metaRaw interface{}) erro token, _, err := client.ld.AccessTokensApi.PostToken(client.ctx).AccessTokenPost(accessTokenBody).Execute() if err != nil { - return fmt.Errorf("failed to create access token with name %q: %s", accessTokenName, handleLdapiErr(err)) + return diag.Errorf("failed to create access token with name %q: %s", accessTokenName, handleLdapiErr(err)) } _ = d.Set(TOKEN, token.Token) d.SetId(token.Id) - return resourceAccessTokenRead(d, metaRaw) + return resourceAccessTokenRead(ctx, d, metaRaw) } -func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error { +func resourceAccessTokenRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) accessTokenID := d.Id() @@ -175,11 +179,15 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error if isStatusNotFound(res) { log.Printf("[WARN] failed to find access token with id %q, removing from state", accessTokenID) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find access token with id %q, removing from state", accessTokenID), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get access token with id %q: %s", accessTokenID, handleLdapiErr(err)) + return diag.Errorf("failed to get access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } _ = d.Set(NAME, accessToken.Name) @@ -189,7 +197,7 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error if accessToken.CustomRoleIds != nil && len(*accessToken.CustomRoleIds) > 0 { customRoleKeys, err := customRoleIDsToKeys(client, *accessToken.CustomRoleIds) if err != nil { - return err + return diag.FromErr(err) } _ = d.Set(CUSTOM_ROLES, customRoleKeys) } @@ -205,17 +213,17 @@ func resourceAccessTokenRead(d *schema.ResourceData, metaRaw interface{}) error err = d.Set(INLINE_ROLES, policyStatementsToResourceData(*policies)) } if err != nil { - return fmt.Errorf("could not set policy on access token with id %q: %v", accessTokenID, err) + return diag.Errorf("could not set policy on access token with id %q: %v", accessTokenID, err) } } - return nil + return diags } -func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceAccessTokenUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { err := validateAccessTokenResource(d) if err != nil { - return err + return diag.FromErr(err) } client := metaRaw.(*Client) @@ -230,7 +238,7 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro } customRoleIds, err := customRoleKeysToIDs(client, customRoleKeys) if err != nil { - return err + return diag.FromErr(err) } inlineRoles, _ := policyStatementsFromResourceData(d.Get(POLICY_STATEMENTS).([]interface{})) @@ -273,7 +281,7 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err = client.ld.AccessTokensApi.PatchToken(client.ctx, accessTokenID).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update access token with id %q: %s", accessTokenID, handleLdapiErr(err)) + return diag.Errorf("failed to update access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } // Reset the access token if the expire field has been updated @@ -284,27 +292,29 @@ func resourceAccessTokenUpdate(d *schema.ResourceData, metaRaw interface{}) erro if oldExpire != newExpire && newExpire != 0 { token, err := resetAccessToken(client, accessTokenID, newExpire) if err != nil { - return fmt.Errorf("failed to reset access token with id %q: %s", accessTokenID, handleLdapiErr(err)) + return diag.Errorf("failed to reset access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } _ = d.Set(EXPIRE, newExpire) _ = d.Set(TOKEN, token.Token) } } - return resourceAccessTokenRead(d, metaRaw) + return resourceAccessTokenRead(ctx, d, metaRaw) } -func resourceAccessTokenDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceAccessTokenDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) accessTokenID := d.Id() _, err := client.ld.AccessTokensApi.DeleteToken(client.ctx, accessTokenID).Execute() if err != nil { - return fmt.Errorf("failed to delete access token with id %q: %s", accessTokenID, handleLdapiErr(err)) + return diag.Errorf("failed to delete access token with id %q: %s", accessTokenID, handleLdapiErr(err)) } - return nil + return diags } func resourceAccessTokenExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_custom_role.go b/launchdarkly/resource_launchdarkly_custom_role.go index e760f1fc..0fc0686d 100644 --- a/launchdarkly/resource_launchdarkly_custom_role.go +++ b/launchdarkly/resource_launchdarkly_custom_role.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -11,11 +13,11 @@ import ( func resourceCustomRole() *schema.Resource { return &schema.Resource{ - Create: resourceCustomRoleCreate, - Read: resourceCustomRoleRead, - Update: resourceCustomRoleUpdate, - Delete: resourceCustomRoleDelete, - Exists: resourceCustomRoleExists, + CreateContext: resourceCustomRoleCreate, + ReadContext: resourceCustomRoleRead, + UpdateContext: resourceCustomRoleUpdate, + DeleteContext: resourceCustomRoleDelete, + Exists: resourceCustomRoleExists, Importer: &schema.ResourceImporter{ State: resourceCustomRoleImport, @@ -23,11 +25,11 @@ func resourceCustomRole() *schema.Resource { Schema: map[string]*schema.Schema{ KEY: { - Type: schema.TypeString, - Required: true, - Description: "A unique key that will be used to reference the custom role in your code", - ForceNew: true, - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + Description: "A unique key that will be used to reference the custom role in your code", + ForceNew: true, + ValidateDiagFunc: validateKey(), }, NAME: { Type: schema.TypeString, @@ -45,7 +47,7 @@ func resourceCustomRole() *schema.Resource { } } -func resourceCustomRoleCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceCustomRoleCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) customRoleKey := d.Get(KEY).(string) customRoleName := d.Get(NAME).(string) @@ -53,7 +55,7 @@ func resourceCustomRoleCreate(d *schema.ResourceData, metaRaw interface{}) error customRolePolicies := policiesFromResourceData(d) policyStatements, err := policyStatementsFromResourceData(d.Get(POLICY_STATEMENTS).([]interface{})) if err != nil { - return err + return diag.FromErr(err) } if len(policyStatements) > 0 { customRolePolicies = policyStatements @@ -69,14 +71,16 @@ func resourceCustomRoleCreate(d *schema.ResourceData, metaRaw interface{}) error _, _, err = client.ld.CustomRolesApi.PostCustomRole(client.ctx).CustomRolePost(customRoleBody).Execute() if err != nil { - return fmt.Errorf("failed to create custom role with name %q: %s", customRoleName, handleLdapiErr(err)) + return diag.Errorf("failed to create custom role with name %q: %s", customRoleName, handleLdapiErr(err)) } d.SetId(customRoleKey) - return resourceCustomRoleRead(d, metaRaw) + return resourceCustomRoleRead(ctx, d, metaRaw) } -func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { +func resourceCustomRoleRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) customRoleID := d.Id() @@ -84,11 +88,15 @@ func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { if isStatusNotFound(res) { log.Printf("[WARN] failed to find custom role with id %q, removing from state", customRoleID) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find custom role with id %q, removing from state", customRoleID), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get custom role with id %q: %s", customRoleID, handleLdapiErr(err)) + return diag.Errorf("failed to get custom role with id %q: %s", customRoleID, handleLdapiErr(err)) } _ = d.Set(KEY, customRole.Key) @@ -104,12 +112,12 @@ func resourceCustomRoleRead(d *schema.ResourceData, metaRaw interface{}) error { } if err != nil { - return fmt.Errorf("could not set policy on custom role with id %q: %v", customRoleID, err) + return diag.Errorf("could not set policy on custom role with id %q: %v", customRoleID, err) } return nil } -func resourceCustomRoleUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceCustomRoleUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) customRoleKey := d.Get(KEY).(string) customRoleName := d.Get(NAME).(string) @@ -117,7 +125,7 @@ func resourceCustomRoleUpdate(d *schema.ResourceData, metaRaw interface{}) error customRolePolicies := policiesFromResourceData(d) policyStatements, err := policyStatementsFromResourceData(d.Get(POLICY_STATEMENTS).([]interface{})) if err != nil { - return err + return diag.FromErr(err) } if len(policyStatements) > 0 { customRolePolicies = policyStatements @@ -132,23 +140,25 @@ func resourceCustomRoleUpdate(d *schema.ResourceData, metaRaw interface{}) error _, _, err = client.ld.CustomRolesApi.PatchCustomRole(client.ctx, customRoleKey).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) + return diag.Errorf("failed to update custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) } - return resourceCustomRoleRead(d, metaRaw) + return resourceCustomRoleRead(ctx, d, metaRaw) } -func resourceCustomRoleDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceCustomRoleDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) customRoleKey := d.Id() _, err := client.ld.CustomRolesApi.DeleteCustomRole(client.ctx, customRoleKey).Execute() if err != nil { - return fmt.Errorf("failed to delete custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete custom role with key %q: %s", customRoleKey, handleLdapiErr(err)) } - return nil + return diags } func resourceCustomRoleExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_destination.go b/launchdarkly/resource_launchdarkly_destination.go index bea08b7a..2e3320b6 100644 --- a/launchdarkly/resource_launchdarkly_destination.go +++ b/launchdarkly/resource_launchdarkly_destination.go @@ -1,10 +1,12 @@ package launchdarkly import ( + "context" "fmt" "log" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go/v7" @@ -12,11 +14,11 @@ import ( func resourceDestination() *schema.Resource { return &schema.Resource{ - Create: resourceDestinationCreate, - Read: resourceDestinationRead, - Update: resourceDestinationUpdate, - Delete: resourceDestinationDelete, - Exists: resourceDestinationExists, + CreateContext: resourceDestinationCreate, + ReadContext: resourceDestinationRead, + UpdateContext: resourceDestinationUpdate, + DeleteContext: resourceDestinationDelete, + Exists: resourceDestinationExists, Importer: &schema.ResourceImporter{ State: resourceDestinationImport, @@ -24,11 +26,11 @@ func resourceDestination() *schema.Resource { Schema: map[string]*schema.Schema{ PROJECT_KEY: { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The LaunchDarkly project key", - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The LaunchDarkly project key", + ValidateDiagFunc: validateKey(), }, ENV_KEY: { Type: schema.TypeString, @@ -43,11 +45,11 @@ func resourceDestination() *schema.Resource { }, // kind can only be one of five types (kinesis, google-pubsub, mparticle, azure-event-hubs, or segment) KIND: { - Type: schema.TypeString, - Required: true, - Description: "The data export destination type. Available choices are 'kinesis', 'google-pubsub', 'segment', 'azure-event-hubs', and 'mparticle'", - ValidateFunc: validation.StringInSlice([]string{"kinesis", "google-pubsub", "mparticle", "azure-event-hubs", "segment"}, false), - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "The data export destination type. Available choices are 'kinesis', 'google-pubsub', 'segment', 'azure-event-hubs', and 'mparticle'", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"kinesis", "google-pubsub", "mparticle", "azure-event-hubs", "segment"}, false)), + ForceNew: true, }, CONFIG: { Type: schema.TypeMap, @@ -65,7 +67,7 @@ func resourceDestination() *schema.Resource { } } -func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceDestinationCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) @@ -75,7 +77,7 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro destinationConfig, err := destinationConfigFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } destinationBody := ldapi.DestinationPost{ @@ -88,20 +90,21 @@ func resourceDestinationCreate(d *schema.ResourceData, metaRaw interface{}) erro destination, _, err := client.ld.DataExportDestinationsApi.PostDestination(client.ctx, destinationProjKey, destinationEnvKey).DestinationPost(destinationBody).Execute() if err != nil { d.SetId("") - return fmt.Errorf("failed to create destination with project key %q and env key %q: %s", destinationProjKey, destinationEnvKey, handleLdapiErr(err)) + return diag.Errorf("failed to create destination with project key %q and env key %q: %s", destinationProjKey, destinationEnvKey, handleLdapiErr(err)) } // destination defined in api-client-go/model_destination.go d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, *destination.Id}, "/")) - return resourceDestinationRead(d, metaRaw) + return resourceDestinationRead(ctx, d, metaRaw) } -func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error { +func resourceDestinationRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics client := metaRaw.(*Client) _, _, destinationID, err := destinationImportIDtoKeys(d.Id()) if err != nil { - return err + return diag.FromErr(err) } destinationProjKey := d.Get(PROJECT_KEY).(string) @@ -111,11 +114,15 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error if isStatusNotFound(res) { log.Printf("[WARN] failed to find destination with id: %q in project %q, environment: %q, removing from state", destinationID, destinationProjKey, destinationEnvKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find destination with id: %q in project %q, environment: %q, removing from state", destinationID, destinationProjKey, destinationEnvKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get destination with id %q: %s", destinationID, handleLdapiErr(err)) + return diag.Errorf("failed to get destination with id %q: %s", destinationID, handleLdapiErr(err)) } cfg := destinationConfigToResourceData(*destination.Kind, destination.Config) @@ -127,14 +134,14 @@ func resourceDestinationRead(d *schema.ResourceData, metaRaw interface{}) error _ = d.Set(ON, destination.On) d.SetId(strings.Join([]string{destinationProjKey, destinationEnvKey, *destination.Id}, "/")) - return nil + return diags } -func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceDestinationUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) _, _, destinationID, err := destinationImportIDtoKeys(d.Id()) if err != nil { - return err + return diag.FromErr(err) } destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) @@ -142,7 +149,7 @@ func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) erro destinationKind := d.Get(KIND).(string) destinationConfig, err := destinationConfigFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } destinationOn := d.Get(ON).(bool) @@ -155,27 +162,29 @@ func resourceDestinationUpdate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err = client.ld.DataExportDestinationsApi.PatchDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update destination with id %q: %s", destinationID, handleLdapiErr(err)) + return diag.Errorf("failed to update destination with id %q: %s", destinationID, handleLdapiErr(err)) } - return resourceDestinationRead(d, metaRaw) + return resourceDestinationRead(ctx, d, metaRaw) } -func resourceDestinationDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceDestinationDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) _, _, destinationID, err := destinationImportIDtoKeys(d.Id()) if err != nil { - return err + return diag.FromErr(err) } destinationProjKey := d.Get(PROJECT_KEY).(string) destinationEnvKey := d.Get(ENV_KEY).(string) _, err = client.ld.DataExportDestinationsApi.DeleteDestination(client.ctx, destinationProjKey, destinationEnvKey, destinationID).Execute() if err != nil { - return fmt.Errorf("failed to delete destination with id %q: %s", destinationID, handleLdapiErr(err)) + return diag.Errorf("failed to delete destination with id %q: %s", destinationID, handleLdapiErr(err)) } - return nil + return diags } func resourceDestinationExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_environment.go b/launchdarkly/resource_launchdarkly_environment.go index 961a794f..c4e507a1 100644 --- a/launchdarkly/resource_launchdarkly_environment.go +++ b/launchdarkly/resource_launchdarkly_environment.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "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/v7" ) @@ -11,19 +13,19 @@ import ( func resourceEnvironment() *schema.Resource { envSchema := environmentSchema(false) envSchema[PROJECT_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: "The LaunchDarkly project key", - ForceNew: true, - ValidateFunc: validateKey(), + Type: schema.TypeString, + Required: true, + Description: "The LaunchDarkly project key", + ForceNew: true, + ValidateDiagFunc: validateKey(), } return &schema.Resource{ - Create: resourceEnvironmentCreate, - Read: resourceEnvironmentRead, - Update: resourceEnvironmentUpdate, - Delete: resourceEnvironmentDelete, - Exists: resourceEnvironmentExists, + CreateContext: resourceEnvironmentCreate, + ReadContext: resourceEnvironmentRead, + UpdateContext: resourceEnvironmentUpdate, + DeleteContext: resourceEnvironmentDelete, + Exists: resourceEnvironmentExists, Importer: &schema.ResourceImporter{ State: resourceEnvironmentImport, @@ -32,7 +34,7 @@ func resourceEnvironment() *schema.Resource { } } -func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceEnvironmentCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) projectKey := d.Get(PROJECT_KEY).(string) key := d.Get(KEY).(string) @@ -59,32 +61,34 @@ func resourceEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err := client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() if err != nil { - return fmt.Errorf("failed to create environment: [%+v] for project key: %s: %s", envPost, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to create environment: [%+v] for project key: %s: %s", envPost, projectKey, handleLdapiErr(err)) } approvalSettings := d.Get(APPROVAL_SETTINGS) if len(approvalSettings.([]interface{})) > 0 { - err = resourceEnvironmentUpdate(d, metaRaw) - if err != nil { + updateDiags := resourceEnvironmentUpdate(ctx, d, metaRaw) + if updateDiags.HasError() { // if there was a problem in the update state, we need to clean up completely by deleting the env _, deleteErr := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() + // TODO: Figure out if we can get the err out of updateDiag (not looking likely) to use in hanldeLdapiErr if deleteErr != nil { - return fmt.Errorf("failed to clean up environment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + return updateDiags + // return diag.Errorf("failed to clean up environment %q from project %q: %s", key, projectKey, handleLdapiErr(errs)) } - return fmt.Errorf("failed to update environment with name %q key %q for projectKey %q: %s", + return diag.Errorf("failed to update environment with name %q key %q for projectKey %q: %s", name, key, projectKey, handleLdapiErr(err)) } } d.SetId(projectKey + "/" + key) - return resourceEnvironmentRead(d, metaRaw) + return resourceEnvironmentRead(ctx, d, metaRaw) } -func resourceEnvironmentRead(d *schema.ResourceData, metaRaw interface{}) error { - return environmentRead(d, metaRaw, false) +func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return environmentRead(ctx, d, metaRaw, false) } -func resourceEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) //required fields @@ -110,28 +114,30 @@ func resourceEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) erro oldApprovalSettings, newApprovalSettings := d.GetChange(APPROVAL_SETTINGS) approvalPatch, err := approvalPatchFromSettings(oldApprovalSettings, newApprovalSettings) if err != nil { - return err + return diag.FromErr(err) } patch = append(patch, approvalPatch...) _, _, err = client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, key).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update environment with key %q for project: %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to update environment with key %q for project: %q: %s", key, projectKey, handleLdapiErr(err)) } - return resourceEnvironmentRead(d, metaRaw) + return resourceEnvironmentRead(ctx, d, metaRaw) } -func resourceEnvironmentDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceEnvironmentDelete(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) _, err := client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, key).Execute() if err != nil { - return fmt.Errorf("failed to delete project with key %q for project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete project with key %q for project %q: %s", key, projectKey, handleLdapiErr(err)) } - return nil + return diags } func resourceEnvironmentExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_feature_flag.go b/launchdarkly/resource_launchdarkly_feature_flag.go index e30282bd..6767112c 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag.go +++ b/launchdarkly/resource_launchdarkly_feature_flag.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" ) @@ -65,11 +66,11 @@ func resourceFeatureFlag() *schema.Resource { } schemaMap[VARIATION_TYPE] = variationTypeSchema() return &schema.Resource{ - Create: resourceFeatureFlagCreate, - Read: resourceFeatureFlagRead, - Update: resourceFeatureFlagUpdate, - Delete: resourceFeatureFlagDelete, - Exists: resourceFeatureFlagExists, + CreateContext: resourceFeatureFlagCreate, + ReadContext: resourceFeatureFlagRead, + UpdateContext: resourceFeatureFlagUpdate, + DeleteContext: resourceFeatureFlagDelete, + Exists: resourceFeatureFlagExists, Importer: &schema.ResourceImporter{ State: resourceFeatureFlagImport, @@ -79,15 +80,15 @@ func resourceFeatureFlag() *schema.Resource { } } -func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagCreate(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 err + return diag.FromErr(err) } - return fmt.Errorf("cannot find project with key %q", projectKey) + return diag.Errorf("cannot find project with key %q", projectKey) } key := d.Get(KEY).(string) @@ -108,12 +109,12 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro variations, err := variationsFromResourceData(d) if err != nil { - return fmt.Errorf("invalid variations: %v", err) + return diag.Errorf("invalid variations: %v", err) } defaults, err := defaultVariationsFromResourceData(d) if err != nil { - return fmt.Errorf("invalid default variations: %v", err) + return diag.Errorf("invalid default variations: %v", err) } flag := ldapi.FeatureFlagBody{ @@ -139,7 +140,7 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro // IncludeInSnippetdefault is the same as defaultCSA.UsingEnvironmentId, so we can _ it defaultCSA, _, err := getProjectDefaultCSAandIncludeInSnippet(client, projectKey) if err != nil { - return fmt.Errorf("failed to get project level client side availability defaults. %v", err) + return diag.Errorf("failed to get project level client side availability defaults. %v", err) } flag.ClientSideAvailability = &ldapi.ClientSideAvailabilityPost{ UsingEnvironmentId: *defaultCSA.UsingEnvironmentId, @@ -148,31 +149,33 @@ func resourceFeatureFlagCreate(d *schema.ResourceData, metaRaw interface{}) erro } _, _, err = client.ld.FeatureFlagsApi.PostFeatureFlag(client.ctx, projectKey).FeatureFlagBody(flag).Execute() if err != nil { - return fmt.Errorf("failed to create flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to create flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } // ld's api does not allow some fields to be passed in during flag creation so we do an update: // https://apidocs.launchdarkly.com/docs/create-feature-flag - err = resourceFeatureFlagUpdate(d, metaRaw) - if err != nil { + updateDiags := resourceFeatureFlagUpdate(ctx, d, metaRaw) + if updateDiags.HasError() { // if there was a problem in the update state, we need to clean up completely by deleting the flag _, deleteErr := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() if deleteErr != nil { - return fmt.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(deleteErr)) } - return fmt.Errorf("failed to update flag with name %q key %q for projectKey %q: %s", - flagName, key, projectKey, handleLdapiErr(err)) + // TODO: Figure out if we can get the err out of updateDiag (not looking likely) to use in hanldeLdapiErr + return updateDiags + // return diag.Errorf("failed to update flag with name %q key %q for projectKey %q: %s", + // flagName, key, projectKey, handleLdapiErr(errs)) } d.SetId(projectKey + "/" + key) - return resourceFeatureFlagRead(d, metaRaw) + return resourceFeatureFlagRead(ctx, d, metaRaw) } -func resourceFeatureFlagRead(d *schema.ResourceData, metaRaw interface{}) error { - return featureFlagRead(d, metaRaw, false) +func resourceFeatureFlagRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return featureFlagRead(ctx, d, metaRaw, false) } -func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) key := d.Get(KEY).(string) projectKey := d.Get(PROJECT_KEY).(string) @@ -221,7 +224,7 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro // IncludeInSnippetdefault is the same as defaultCSA.UsingEnvironmentId, so we can _ it defaultCSA, _, err := getProjectDefaultCSAandIncludeInSnippet(client, projectKey) if err != nil { - return fmt.Errorf("failed to get project level client side availability defaults. %v", err) + return diag.Errorf("failed to get project level client side availability defaults. %v", err) } patch.Patch = append(patch.Patch, patchReplace("/clientSideAvailability", &ldapi.ClientSideAvailabilityPost{ UsingEnvironmentId: *defaultCSA.UsingEnvironmentId, @@ -231,14 +234,14 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro variationPatches, err := variationPatchesFromResourceData(d) if err != nil { - return fmt.Errorf("failed to build variation patches. %v", err) + return diag.Errorf("failed to build variation patches. %v", err) } patch.Patch = append(patch.Patch, variationPatches...) // Only update the defaults if they are specified in the schema defaults, err := defaultVariationsFromResourceData(d) if err != nil { - return fmt.Errorf("invalid default variations: %v", err) + return diag.Errorf("invalid default variations: %v", err) } if defaults != nil { patch.Patch = append(patch.Patch, patchReplace("/defaults", defaults)) @@ -252,23 +255,25 @@ func resourceFeatureFlagUpdate(d *schema.ResourceData, metaRaw interface{}) erro _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, key).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to update flag %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } - return resourceFeatureFlagRead(d, metaRaw) + return resourceFeatureFlagRead(ctx, d, metaRaw) } -func resourceFeatureFlagDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagDelete(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) _, err := client.ld.FeatureFlagsApi.DeleteFeatureFlag(client.ctx, projectKey, key).Execute() if err != nil { - return fmt.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete flag %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } - return nil + return diags } func resourceFeatureFlagExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_feature_flag_environment.go b/launchdarkly/resource_launchdarkly_feature_flag_environment.go index 823425c5..83db34dc 100644 --- a/launchdarkly/resource_launchdarkly_feature_flag_environment.go +++ b/launchdarkly/resource_launchdarkly_feature_flag_environment.go @@ -1,20 +1,22 @@ package launchdarkly import ( + "context" "fmt" "log" "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/v7" ) func resourceFeatureFlagEnvironment() *schema.Resource { return &schema.Resource{ - Create: resourceFeatureFlagEnvironmentCreate, - Read: resourceFeatureFlagEnvironmentRead, - Update: resourceFeatureFlagEnvironmentUpdate, - Delete: resourceFeatureFlagEnvironmentDelete, + CreateContext: resourceFeatureFlagEnvironmentCreate, + ReadContext: resourceFeatureFlagEnvironmentRead, + UpdateContext: resourceFeatureFlagEnvironmentUpdate, + DeleteContext: resourceFeatureFlagEnvironmentDelete, Importer: &schema.ResourceImporter{ State: resourceFeatureFlagEnvironmentImport, @@ -29,7 +31,7 @@ func validateFlagID(val interface{}, key string) (warns []string, errs []error) return warns, append(errs, fmt.Errorf("%q must be in the format 'project_key/flag_key'. Got: %s", key, v)) } for _, part := range strings.SplitN(v, "/", 2) { - w, e := validateKey()(part, key) + w, e := validateKeyNoDiag()(part, key) if len(e) > 0 { return w, e } @@ -37,28 +39,28 @@ func validateFlagID(val interface{}, key string) (warns []string, errs []error) return warns, errs } -func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagEnvironmentCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) flagId := d.Get(FLAG_ID).(string) projectKey, flagKey, err := flagIdToKeys(flagId) if err != nil { - return err + return diag.FromErr(err) } envKey := d.Get(ENV_KEY).(string) if exists, err := projectExists(projectKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("cannot find project with key %q", projectKey) + return diag.Errorf("cannot find project with key %q", projectKey) } if exists, err := environmentExists(projectKey, envKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("failed to find environment with key %q", envKey) + return diag.Errorf("failed to find environment with key %q", envKey) } patches := make([]ldapi.PatchOperation, 0) @@ -79,7 +81,7 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf if ok { rules, err := rulesFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } patches = append(patches, patchReplace(patchFlagEnvPath(d, "rules"), rules)) } @@ -99,7 +101,7 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf // fallthrough is required fall, err := fallthroughFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } patches = append(patches, patchReplace(patchFlagEnvPath(d, "fallthrough"), fall)) @@ -113,45 +115,45 @@ func resourceFeatureFlagEnvironmentCreate(d *schema.ResourceData, metaRaw interf _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update flag %q in project %q: %s", flagKey, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to update flag %q in project %q: %s", flagKey, projectKey, handleLdapiErr(err)) } } d.SetId(projectKey + "/" + envKey + "/" + flagKey) - return resourceFeatureFlagEnvironmentRead(d, metaRaw) + return resourceFeatureFlagEnvironmentRead(ctx, d, metaRaw) } -func resourceFeatureFlagEnvironmentRead(d *schema.ResourceData, metaRaw interface{}) error { - return featureFlagEnvironmentRead(d, metaRaw, false) +func resourceFeatureFlagEnvironmentRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return featureFlagEnvironmentRead(ctx, d, metaRaw, false) } -func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) flagId := d.Get(FLAG_ID).(string) projectKey, flagKey, err := flagIdToKeys(flagId) if err != nil { - return err + return diag.FromErr(err) } envKey := d.Get(ENV_KEY).(string) if exists, err := projectExists(projectKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("cannot find project with key %q", projectKey) + return diag.Errorf("cannot find project with key %q", projectKey) } if exists, err := environmentExists(projectKey, envKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("failed to find environment with key %q", envKey) + return diag.Errorf("failed to find environment with key %q", envKey) } on := d.Get(ON) rules, err := rulesFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } trackEvents := d.Get(TRACK_EVENTS).(bool) prerequisites := prerequisitesFromResourceData(d, PREREQUISITES) @@ -159,7 +161,7 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf fall, err := fallthroughFromResourceData(d) if err != nil { - return err + return diag.FromErr(err) } offVariation := d.Get(OFF_VARIATION) @@ -179,37 +181,39 @@ func resourceFeatureFlagEnvironmentUpdate(d *schema.ResourceData, metaRaw interf log.Printf("[DEBUG] %+v\n", patch) _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) + return diag.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } - return resourceFeatureFlagEnvironmentRead(d, metaRaw) + return resourceFeatureFlagEnvironmentRead(ctx, d, metaRaw) } -func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceFeatureFlagEnvironmentDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) flagId := d.Get(FLAG_ID).(string) projectKey, flagKey, err := flagIdToKeys(flagId) if err != nil { - return err + return diag.FromErr(err) } envKey := d.Get(ENV_KEY).(string) if exists, err := projectExists(projectKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("cannot find project with key %q", projectKey) + return diag.Errorf("cannot find project with key %q", projectKey) } if exists, err := environmentExists(projectKey, envKey, client); !exists { if err != nil { - return err + return diag.FromErr(err) } - return fmt.Errorf("failed to find environment with key %q", envKey) + return diag.Errorf("failed to find environment with key %q", envKey) } flag, _, err := client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() if err != nil { - return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) + return diag.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } // Set off variation to match default with how a rule is created @@ -231,10 +235,10 @@ func resourceFeatureFlagEnvironmentDelete(d *schema.ResourceData, metaRaw interf _, _, err = client.ld.FeatureFlagsApi.PatchFeatureFlag(client.ctx, projectKey, flagKey).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) + return diag.Errorf("failed to update flag %q in project %q, environment %q: %s", flagKey, projectKey, envKey, handleLdapiErr(err)) } - return nil + return diags } func resourceFeatureFlagEnvironmentImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { diff --git a/launchdarkly/resource_launchdarkly_project.go b/launchdarkly/resource_launchdarkly_project.go index 725d5c90..8a479cbb 100644 --- a/launchdarkly/resource_launchdarkly_project.go +++ b/launchdarkly/resource_launchdarkly_project.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" ) @@ -51,11 +52,11 @@ func customizeProjectDiff(ctx context.Context, diff *schema.ResourceDiff, v inte } func resourceProject() *schema.Resource { return &schema.Resource{ - Create: resourceProjectCreate, - Read: resourceProjectRead, - Update: resourceProjectUpdate, - Delete: resourceProjectDelete, - Exists: resourceProjectExists, + CreateContext: resourceProjectCreate, + ReadContext: resourceProjectRead, + UpdateContext: resourceProjectUpdate, + DeleteContext: resourceProjectDelete, + Exists: resourceProjectExists, CustomizeDiff: customizeProjectDiff, @@ -121,7 +122,8 @@ func resourceProject() *schema.Resource { } } -func resourceProjectCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics client := metaRaw.(*Client) projectKey := d.Get(KEY).(string) name := d.Get(NAME).(string) @@ -139,22 +141,26 @@ func resourceProjectCreate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err := client.ld.ProjectsApi.PostProject(client.ctx).ProjectPost(projectBody).Execute() if err != nil { - return fmt.Errorf("failed to create project with name %s and projectKey %s: %v", name, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to create project with name %s and projectKey %s: %v", name, projectKey, handleLdapiErr(err)) } // ld's api does not allow tags to be passed in during project creation so we do an update - err = resourceProjectUpdate(d, metaRaw) - if err != nil { - return fmt.Errorf("failed to update project with name %s and projectKey %s: %v", name, projectKey, err) + updateDiags := resourceProjectUpdate(ctx, d, metaRaw) + if updateDiags.HasError() { + updateDiags = append(updateDiags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("failed to update project with name %s and projectKey %s: %v", name, projectKey, err), + }) + return updateDiags } - return nil + return diags } -func resourceProjectRead(d *schema.ResourceData, metaRaw interface{}) error { - return projectRead(d, metaRaw, false) +func resourceProjectRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return projectRead(ctx, d, metaRaw, false) } -func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) projectKey := d.Get(KEY).(string) projName := d.Get(NAME) @@ -196,14 +202,14 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err := client.ld.ProjectsApi.PatchProject(client.ctx, projectKey).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update project with key %q: %s", projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to update project with key %q: %s", projectKey, handleLdapiErr(err)) } // Update environments if necessary oldSchemaEnvList, newSchemaEnvList := d.GetChange(ENVIRONMENTS) // Get the project so we can see if we need to create any environments or just update existing environments project, _, err := client.ld.ProjectsApi.GetProject(client.ctx, projectKey).Execute() if err != nil { - return fmt.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to load project %q before updating environments: %s", projectKey, handleLdapiErr(err)) } environmentConfigs := newSchemaEnvList.([]interface{}) @@ -227,7 +233,7 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { envPost := environmentPostFromResourceData(env) _, _, err := client.ld.EnvironmentsApi.PostEnvironment(client.ctx, projectKey).EnvironmentPost(envPost).Execute() if err != nil { - return fmt.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to create environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) } } @@ -238,11 +244,11 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { // by default patching an env that was not recently tracked in the state will import it into the tf state patch, err := getEnvironmentUpdatePatches(oldEnvConfig, envConfig) if err != nil { - return err + return diag.FromErr(err) } _, _, err = client.ld.EnvironmentsApi.PatchEnvironment(client.ctx, projectKey, envKey).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update environment with key %q for project: %q: %+v", envKey, projectKey, err) + return diag.Errorf("failed to update environment with key %q for project: %q: %+v", envKey, projectKey, err) } } // we also want to delete environments that were previously tracked in state and have been removed from the config @@ -254,24 +260,26 @@ func resourceProjectUpdate(d *schema.ResourceData, metaRaw interface{}) error { if _, persists := envConfigsForCompare[envKey]; !persists { _, err = client.ld.EnvironmentsApi.DeleteEnvironment(client.ctx, projectKey, envKey).Execute() if err != nil { - return fmt.Errorf("failed to delete environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete environment %q in project %q: %s", envKey, projectKey, handleLdapiErr(err)) } } } - return resourceProjectRead(d, metaRaw) + return resourceProjectRead(ctx, d, metaRaw) } -func resourceProjectDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) projectKey := d.Get(KEY).(string) _, err := client.ld.ProjectsApi.DeleteProject(client.ctx, projectKey).Execute() if err != nil { - return fmt.Errorf("failed to delete project with key %q: %s", projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete project with key %q: %s", projectKey, handleLdapiErr(err)) } - return nil + return diags } func resourceProjectExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_segment.go b/launchdarkly/resource_launchdarkly_segment.go index e69bec96..98c376a9 100644 --- a/launchdarkly/resource_launchdarkly_segment.go +++ b/launchdarkly/resource_launchdarkly_segment.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "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/v7" ) @@ -11,25 +13,25 @@ import ( func resourceSegment() *schema.Resource { schemaMap := baseSegmentSchema() schemaMap[PROJECT_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateKey(), - Description: "The segment's project key.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateKey(), + Description: "The segment's project key.", } schemaMap[ENV_KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateKey(), - Description: "The segment's environment key.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateKey(), + Description: "The segment's environment key.", } schemaMap[KEY] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateKey(), - Description: "The unique key that references the segment.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateKey(), + Description: "The unique key that references the segment.", } schemaMap[NAME] = &schema.Schema{ Type: schema.TypeString, @@ -37,11 +39,11 @@ func resourceSegment() *schema.Resource { Description: "The human-friendly name for the segment.", } return &schema.Resource{ - Create: resourceSegmentCreate, - Read: resourceSegmentRead, - Update: resourceSegmentUpdate, - Delete: resourceSegmentDelete, - Exists: resourceSegmentExists, + CreateContext: resourceSegmentCreate, + ReadContext: resourceSegmentRead, + UpdateContext: resourceSegmentUpdate, + DeleteContext: resourceSegmentDelete, + Exists: resourceSegmentExists, Importer: &schema.ResourceImporter{ State: resourceSegmentImport, @@ -51,7 +53,7 @@ func resourceSegment() *schema.Resource { } } -func resourceSegmentCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceSegmentCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) projectKey := d.Get(PROJECT_KEY).(string) envKey := d.Get(ENV_KEY).(string) @@ -70,26 +72,28 @@ func resourceSegmentCreate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err := client.ld.SegmentsApi.PostSegment(client.ctx, projectKey, envKey).SegmentBody(segment).Execute() if err != nil { - return fmt.Errorf("failed to create segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to create segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } // ld's api does not allow some fields to be passed in during segment creation so we do an update: // https://apidocs.launchdarkly.com/reference#create-segment - err = resourceSegmentUpdate(d, metaRaw) - if err != nil { - return fmt.Errorf("failed to update segment with name %q key %q for projectKey %q: %s", - segmentName, key, projectKey, handleLdapiErr(err)) + updateDiags := resourceSegmentUpdate(ctx, d, metaRaw) + if updateDiags.HasError() { + // TODO: Figure out if we can get the err out of updateDiag (not looking likely) to use in hanldeLdapiErr + return updateDiags + // return diag.Errorf("failed to update segment with name %q key %q for projectKey %q: %s", + // segmentName, key, projectKey, handleLdapiErr(errs)) } d.SetId(projectKey + "/" + envKey + "/" + key) - return resourceSegmentRead(d, metaRaw) + return resourceSegmentRead(ctx, d, metaRaw) } -func resourceSegmentRead(d *schema.ResourceData, metaRaw interface{}) error { - return segmentRead(d, metaRaw, false) +func resourceSegmentRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return segmentRead(ctx, d, metaRaw, false) } -func resourceSegmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceSegmentUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) key := d.Get(KEY).(string) projectKey := d.Get(PROJECT_KEY).(string) @@ -101,7 +105,7 @@ func resourceSegmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { excluded := d.Get(EXCLUDED).([]interface{}) rules, err := segmentRulesFromResourceData(d, RULES) if err != nil { - return err + return diag.FromErr(err) } comment := "Terraform" patch := ldapi.PatchWithComment{ @@ -118,13 +122,15 @@ func resourceSegmentUpdate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err = client.ld.SegmentsApi.PatchSegment(client.ctx, projectKey, envKey, key).PatchWithComment(patch).Execute() if err != nil { - return fmt.Errorf("failed to update segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to update segment %q in project %q: %s", key, projectKey, handleLdapiErr(err)) } - return resourceSegmentRead(d, metaRaw) + return resourceSegmentRead(ctx, d, metaRaw) } -func resourceSegmentDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceSegmentDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) projectKey := d.Get(PROJECT_KEY).(string) envKey := d.Get(ENV_KEY).(string) @@ -132,10 +138,10 @@ func resourceSegmentDelete(d *schema.ResourceData, metaRaw interface{}) error { _, err := client.ld.SegmentsApi.DeleteSegment(client.ctx, projectKey, envKey, key).Execute() if err != nil { - return fmt.Errorf("failed to delete segment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to delete segment %q from project %q: %s", key, projectKey, handleLdapiErr(err)) } - return nil + return diags } func resourceSegmentExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_team_member.go b/launchdarkly/resource_launchdarkly_team_member.go index 3b8e47f8..810b9a96 100644 --- a/launchdarkly/resource_launchdarkly_team_member.go +++ b/launchdarkly/resource_launchdarkly_team_member.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -12,11 +14,11 @@ import ( func resourceTeamMember() *schema.Resource { return &schema.Resource{ - Create: resourceTeamMemberCreate, - Read: resourceTeamMemberRead, - Update: resourceTeamMemberUpdate, - Delete: resourceTeamMemberDelete, - Exists: resourceTeamMemberExists, + CreateContext: resourceTeamMemberCreate, + ReadContext: resourceTeamMemberRead, + UpdateContext: resourceTeamMemberUpdate, + DeleteContext: resourceTeamMemberDelete, + Exists: resourceTeamMemberExists, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -40,12 +42,12 @@ func resourceTeamMember() *schema.Resource { Description: "The team member's last name", }, ROLE: { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "The team member's role. This must be reader, writer, admin, or owner. Team members must have either a role or custom role", - ValidateFunc: validation.StringInSlice([]string{"reader", "writer", "admin"}, false), - AtLeastOneOf: []string{ROLE, CUSTOM_ROLES}, + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The team member's role. This must be reader, writer, admin, or owner. Team members must have either a role or custom role", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"reader", "writer", "admin"}, false)), + AtLeastOneOf: []string{ROLE, CUSTOM_ROLES}, }, CUSTOM_ROLES: { Type: schema.TypeSet, @@ -59,7 +61,7 @@ func resourceTeamMember() *schema.Resource { } } -func resourceTeamMemberCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceTeamMemberCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) memberEmail := d.Get(EMAIL).(string) firstName := d.Get(FIRST_NAME).(string) @@ -82,25 +84,31 @@ func resourceTeamMemberCreate(d *schema.ResourceData, metaRaw interface{}) error members, _, err := client.ld.AccountMembersApi.PostMembers(client.ctx).NewMemberForm([]ldapi.NewMemberForm{membersBody}).Execute() if err != nil { - return fmt.Errorf("failed to create team member with email: %s: %v", memberEmail, handleLdapiErr(err)) + return diag.Errorf("failed to create team member with email: %s: %v", memberEmail, handleLdapiErr(err)) } d.SetId(members.Items[0].Id) - return resourceTeamMemberRead(d, metaRaw) + return resourceTeamMemberRead(ctx, d, metaRaw) } -func resourceTeamMemberRead(d *schema.ResourceData, metaRaw interface{}) error { +func resourceTeamMemberRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) memberID := d.Id() member, res, err := client.ld.AccountMembersApi.GetMember(client.ctx, memberID).Execute() if isStatusNotFound(res) { log.Printf("[WARN] failed to find member with id %q, removing from state", memberID) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find member with id %q, removing from state", memberID), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get member with id %q: %v", memberID, err) + return diag.Errorf("failed to get member with id %q: %v", memberID, err) } d.SetId(member.Id) @@ -111,16 +119,16 @@ func resourceTeamMemberRead(d *schema.ResourceData, metaRaw interface{}) error { customRoleKeys, err := customRoleIDsToKeys(client, member.CustomRoles) if err != nil { - return err + return diag.FromErr(err) } err = d.Set(CUSTOM_ROLES, customRoleKeys) if err != nil { - return fmt.Errorf("failed to set custom roles on team member with id %q: %v", member.Id, err) + return diag.Errorf("failed to set custom roles on team member with id %q: %v", member.Id, err) } - return nil + return diags } -func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceTeamMemberUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) memberID := d.Id() memberRole := d.Get(ROLE).(string) @@ -132,7 +140,7 @@ func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error } customRoleIds, err := customRoleKeysToIDs(client, customRoleKeys) if err != nil { - return err + return diag.FromErr(err) } patch := []ldapi.PatchOperation{ @@ -143,21 +151,23 @@ func resourceTeamMemberUpdate(d *schema.ResourceData, metaRaw interface{}) error _, _, err = client.ld.AccountMembersApi.PatchMember(client.ctx, memberID).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update team member with id %q: %s", memberID, handleLdapiErr(err)) + return diag.Errorf("failed to update team member with id %q: %s", memberID, handleLdapiErr(err)) } - return resourceTeamMemberRead(d, metaRaw) + return resourceTeamMemberRead(ctx, d, metaRaw) } -func resourceTeamMemberDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceTeamMemberDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) _, err := client.ld.AccountMembersApi.DeleteMember(client.ctx, d.Id()).Execute() if err != nil { - return fmt.Errorf("failed to delete team member with id %q: %s", d.Id(), handleLdapiErr(err)) + return diag.Errorf("failed to delete team member with id %q: %s", d.Id(), handleLdapiErr(err)) } - return nil + return diags } func resourceTeamMemberExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/resource_launchdarkly_webhook.go b/launchdarkly/resource_launchdarkly_webhook.go index 909b2c82..cd943c8c 100644 --- a/launchdarkly/resource_launchdarkly_webhook.go +++ b/launchdarkly/resource_launchdarkly_webhook.go @@ -1,8 +1,10 @@ package launchdarkly import ( + "context" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ldapi "github.com/launchdarkly/api-client-go/v7" ) @@ -21,11 +23,11 @@ func resourceWebhook() *schema.Resource { Default: false, } return &schema.Resource{ - Create: resourceWebhookCreate, - Read: resourceWebhookRead, - Update: resourceWebhookUpdate, - Delete: resourceWebhookDelete, - Exists: resourceWebhookExists, + CreateContext: resourceWebhookCreate, + ReadContext: resourceWebhookRead, + UpdateContext: resourceWebhookUpdate, + DeleteContext: resourceWebhookDelete, + Exists: resourceWebhookExists, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -35,7 +37,7 @@ func resourceWebhook() *schema.Resource { } } -func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceWebhookCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) webhookURL := d.Get(URL).(string) webhookSecret := d.Get(SECRET).(string) @@ -52,7 +54,7 @@ func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { if rawStatements, ok := d.GetOk(STATEMENTS); ok { statements, err := policyStatementsFromResourceData(rawStatements.([]interface{})) if err != nil { - return err + return diag.FromErr(err) } webhookBody.Statements = &statements } @@ -66,25 +68,29 @@ func resourceWebhookCreate(d *schema.ResourceData, metaRaw interface{}) error { webhook, _, err := client.ld.WebhooksApi.PostWebhook(client.ctx).WebhookPost(webhookBody).Execute() if err != nil { - return fmt.Errorf("failed to create webhook with name %q: %s", webhookName, handleLdapiErr(err)) + return diag.Errorf("failed to create webhook with name %q: %s", webhookName, handleLdapiErr(err)) } d.SetId(webhook.Id) // ld's api does not allow tags to be passed in during webhook creation so we do an update - err = resourceWebhookUpdate(d, metaRaw) - if err != nil { - return fmt.Errorf("error updating after webhook creation. Webhook name: %q", webhookName) + updateDiags := resourceWebhookUpdate(ctx, d, metaRaw) + if updateDiags.HasError() { + updateDiags = append(updateDiags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error updating after webhook creation. Webhook name: %q", webhookName), + }) + return updateDiags } - return resourceWebhookRead(d, metaRaw) + return resourceWebhookRead(ctx, d, metaRaw) } -func resourceWebhookRead(d *schema.ResourceData, metaRaw interface{}) error { - return webhookRead(d, metaRaw, false) +func resourceWebhookRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return webhookRead(ctx, d, metaRaw, false) } -func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { +func resourceWebhookUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { client := metaRaw.(*Client) webhookID := d.Id() webhookURL := d.Get(URL).(string) @@ -103,7 +109,7 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) if err != nil { - return err + return diag.FromErr(err) } if d.HasChange(STATEMENTS) { @@ -116,22 +122,24 @@ func resourceWebhookUpdate(d *schema.ResourceData, metaRaw interface{}) error { _, _, err = client.ld.WebhooksApi.PatchWebhook(client.ctx, webhookID).PatchOperation(patch).Execute() if err != nil { - return fmt.Errorf("failed to update webhook with id %q: %s", webhookID, handleLdapiErr(err)) + return diag.Errorf("failed to update webhook with id %q: %s", webhookID, handleLdapiErr(err)) } - return resourceWebhookRead(d, metaRaw) + return resourceWebhookRead(ctx, d, metaRaw) } -func resourceWebhookDelete(d *schema.ResourceData, metaRaw interface{}) error { +func resourceWebhookDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) webhookID := d.Id() _, err := client.ld.WebhooksApi.DeleteWebhook(client.ctx, webhookID).Execute() if err != nil { - return fmt.Errorf("failed to delete webhook with id %q: %s", webhookID, handleLdapiErr(err)) + return diag.Errorf("failed to delete webhook with id %q: %s", webhookID, handleLdapiErr(err)) } - return nil + return diags } func resourceWebhookExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { diff --git a/launchdarkly/rollout_helper.go b/launchdarkly/rollout_helper.go index 69f577f3..9f3c00c9 100644 --- a/launchdarkly/rollout_helper.go +++ b/launchdarkly/rollout_helper.go @@ -14,7 +14,9 @@ func rolloutSchema() *schema.Schema { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ - Type: schema.TypeInt, + Type: schema.TypeInt, + // Can't use validation.ToDiagFunc converted validators on TypeList at the moment + // https://github.com/hashicorp/terraform-plugin-sdk/issues/734 ValidateFunc: validation.IntBetween(0, 100000), }, } diff --git a/launchdarkly/rule_helper.go b/launchdarkly/rule_helper.go index 702ad918..7245933e 100644 --- a/launchdarkly/rule_helper.go +++ b/launchdarkly/rule_helper.go @@ -18,11 +18,11 @@ func rulesSchema() *schema.Schema { Schema: map[string]*schema.Schema{ CLAUSES: clauseSchema(), VARIATION: { - Type: schema.TypeInt, - Elem: &schema.Schema{Type: schema.TypeInt}, - Optional: true, - Description: "The integer variation index to serve if the rule clauses evaluate to true. This argument is only valid if clauses are also specified", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + Description: "The integer variation index to serve if the rule clauses evaluate to true. This argument is only valid if clauses are also specified", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, ROLLOUT_WEIGHTS: rolloutSchema(), BUCKET_BY: { diff --git a/launchdarkly/segment_rule_helper.go b/launchdarkly/segment_rule_helper.go index 55c2d04e..1e8a228d 100644 --- a/launchdarkly/segment_rule_helper.go +++ b/launchdarkly/segment_rule_helper.go @@ -14,11 +14,11 @@ func segmentRulesSchema() *schema.Schema { Schema: map[string]*schema.Schema{ CLAUSES: clauseSchema(), WEIGHT: { - Type: schema.TypeInt, - Elem: &schema.Schema{Type: schema.TypeInt}, - Optional: true, - ValidateFunc: validation.IntBetween(0, 100000), - Description: "The integer weight of the rule (between 0 and 100000).", + Type: schema.TypeInt, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100000)), + Description: "The integer weight of the rule (between 0 and 100000).", }, BUCKET_BY: { Type: schema.TypeString, diff --git a/launchdarkly/segments_helper.go b/launchdarkly/segments_helper.go index 8d1d7f30..34e70335 100644 --- a/launchdarkly/segments_helper.go +++ b/launchdarkly/segments_helper.go @@ -1,9 +1,11 @@ package launchdarkly import ( + "context" "fmt" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -36,7 +38,8 @@ func baseSegmentSchema() map[string]*schema.Schema { } } -func segmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) error { +func segmentRead(ctx context.Context, d *schema.ResourceData, raw interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := raw.(*Client) projectKey := d.Get(PROJECT_KEY).(string) envKey := d.Get(ENV_KEY).(string) @@ -45,11 +48,15 @@ func segmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) err segment, res, err := client.ld.SegmentsApi.GetSegment(client.ctx, projectKey, envKey, segmentKey).Execute() if isStatusNotFound(res) && !isDataSource { log.Printf("[WARN] failed to find segment %q in project %q, environment %q, removing from state", segmentKey, projectKey, envKey) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find segment %q in project %q, environment %q, removing from state", segmentKey, projectKey, envKey), + }) d.SetId("") - return nil + return diags } if err != nil { - return fmt.Errorf("failed to get segment %q of project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) + return diag.Errorf("failed to get segment %q of project %q: %s", segmentKey, projectKey, handleLdapiErr(err)) } if isDataSource { @@ -61,26 +68,26 @@ func segmentRead(d *schema.ResourceData, raw interface{}, isDataSource bool) err err = d.Set(TAGS, segment.Tags) if err != nil { - return fmt.Errorf("failed to set tags on segment with key %q: %v", segmentKey, err) + return diag.Errorf("failed to set tags on segment with key %q: %v", segmentKey, err) } err = d.Set(INCLUDED, segment.Included) if err != nil { - return fmt.Errorf("failed to set included on segment with key %q: %v", segmentKey, err) + return diag.Errorf("failed to set included on segment with key %q: %v", segmentKey, err) } err = d.Set(EXCLUDED, segment.Excluded) if err != nil { - return fmt.Errorf("failed to set excluded on segment with key %q: %v", segmentKey, err) + return diag.Errorf("failed to set excluded on segment with key %q: %v", segmentKey, err) } rules, err := segmentRulesToResourceData(segment.Rules) if err != nil { - return fmt.Errorf("failed to read rules on segment with key %q: %v", segmentKey, err) + return diag.Errorf("failed to read rules on segment with key %q: %v", segmentKey, err) } err = d.Set(RULES, rules) if err != nil { - return fmt.Errorf("failed to set excluded on segment with key %q: %v", segmentKey, err) + return diag.Errorf("failed to set excluded on segment with key %q: %v", segmentKey, err) } - return nil + return diags } diff --git a/launchdarkly/tags_helper.go b/launchdarkly/tags_helper.go index 8c681df5..45621ff1 100644 --- a/launchdarkly/tags_helper.go +++ b/launchdarkly/tags_helper.go @@ -9,8 +9,10 @@ func tagsSchema() *schema.Schema { Type: schema.TypeSet, Set: schema.HashString, Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validateTags(), + Type: schema.TypeString, + // Can't use validation.ToDiagFunc converted validators on TypeSet at the moment + // https://github.com/hashicorp/terraform-plugin-sdk/issues/734 + ValidateFunc: validateTagsNoDiag(), }, Optional: true, Description: "Tags associated with your resource", diff --git a/launchdarkly/target_helper.go b/launchdarkly/target_helper.go index c973a6cc..c03503fb 100644 --- a/launchdarkly/target_helper.go +++ b/launchdarkly/target_helper.go @@ -21,10 +21,10 @@ func targetsSchema() *schema.Schema { Description: "List of user strings to target", }, VARIATION: { - Type: schema.TypeInt, - Required: true, - Description: "Index of the variation to serve if a user_target is matched", - ValidateFunc: validation.IntAtLeast(0), + Type: schema.TypeInt, + Required: true, + Description: "Index of the variation to serve if a user_target is matched", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, diff --git a/launchdarkly/validation_helper.go b/launchdarkly/validation_helper.go index 928f0cbc..fb7e9608 100644 --- a/launchdarkly/validation_helper.go +++ b/launchdarkly/validation_helper.go @@ -7,14 +7,25 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +// Can't use validation.ToDiagFunc converted validators on TypeList at the moment +// https://github.com/hashicorp/terraform-plugin-sdk/issues/734 //nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type -func validateKey() schema.SchemaValidateFunc { +func validateKeyNoDiag() schema.SchemaValidateFunc { return validation.StringMatch( regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`), "Must contain only letters, numbers, '.', '-', or '_' and must start with an alphanumeric", ) } +func validateKey() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.StringMatch( + regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`), + "Must contain only letters, numbers, '.', '-', or '_' and must start with an alphanumeric", + )) +} + +// Can't use validation.ToDiagFunc converted validators on TypeList at the moment +// https://github.com/hashicorp/terraform-plugin-sdk/issues/734 //nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type func validateKeyAndLength(minLength, maxLength int) schema.SchemaValidateFunc { return validation.All( @@ -26,16 +37,17 @@ func validateKeyAndLength(minLength, maxLength int) schema.SchemaValidateFunc { ) } -//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type -func validateID() schema.SchemaValidateFunc { - return validation.All( +func validateID() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.All( validation.StringMatch(regexp.MustCompile(`^[a-fA-F0-9]*$`), "Must be a 24 character hexadecimal string"), validation.StringLenBetween(24, 24), - ) + )) } +// Can't use validation.ToDiagFunc converted validators on TypeList at the moment +// https://github.com/hashicorp/terraform-plugin-sdk/issues/734 //nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type -func validateTags() schema.SchemaValidateFunc { +func validateTagsNoDiag() schema.SchemaValidateFunc { return validation.All( validation.StringLenBetween(1, 64), validation.StringMatch( @@ -45,9 +57,18 @@ func validateTags() schema.SchemaValidateFunc { ) } -//nolint:staticcheck // SA1019 TODO: return SchemaValidateDiagFunc type -func validateOp() schema.SchemaValidateFunc { - return validation.StringInSlice([]string{ +// func validateTags() schema.SchemaValidateDiagFunc { +// return validation.ToDiagFunc(validation.All( +// validation.StringLenBetween(1, 64), +// validation.StringMatch( +// regexp.MustCompile(`^[a-zA-Z0-9_.-]*$`), +// "Must contain only letters, numbers, '.', '-', or '_' and be at most 64 characters", +// ), +// )) +// } + +func validateOp() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.StringInSlice([]string{ "in", "endsWith", "startsWith", @@ -63,5 +84,5 @@ func validateOp() schema.SchemaValidateFunc { "semVerEqual", "semVerLessThan", "semVerGreaterThan", - }, false) + }, false)) } diff --git a/launchdarkly/variations_helper.go b/launchdarkly/variations_helper.go index 5fcf6e27..be201936 100644 --- a/launchdarkly/variations_helper.go +++ b/launchdarkly/variations_helper.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ldapi "github.com/launchdarkly/api-client-go/v7" ) @@ -26,7 +27,7 @@ func variationTypeSchema() *schema.Schema { ForceNew: true, Description: fmt.Sprintf("The uniform type for all variations. Can be either %q, %q, %q, or %q.", BOOL_VARIATION, STRING_VARIATION, NUMBER_VARIATION, JSON_VARIATION), - ValidateFunc: validateVariationType, + ValidateDiagFunc: validation.ToDiagFunc(validateVariationType), } } @@ -50,10 +51,10 @@ func variationsSchema() *schema.Schema { Description: "A description for the variation", }, VALUE: { - Type: schema.TypeString, - Required: true, - Description: "The value of the flag for this variation", - ValidateFunc: validateVariationValue, + Type: schema.TypeString, + Required: true, + Description: "The value of the flag for this variation", + ValidateDiagFunc: validation.ToDiagFunc(validateVariationValue), StateFunc: func(i interface{}) string { // All values are stored as strings in TF state v, err := structure.NormalizeJsonString(i) diff --git a/launchdarkly/webhooks_helper.go b/launchdarkly/webhooks_helper.go index 692c63db..0bd59fc2 100644 --- a/launchdarkly/webhooks_helper.go +++ b/launchdarkly/webhooks_helper.go @@ -1,9 +1,10 @@ package launchdarkly import ( - "fmt" + "context" "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -25,7 +26,8 @@ func baseWebhookSchema() map[string]*schema.Schema { } } -func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) error { +func webhookRead(ctx context.Context, d *schema.ResourceData, meta interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics client := meta.(*Client) var webhookID string if isDataSource { @@ -41,13 +43,13 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er return nil } if err != nil { - return fmt.Errorf("failed to get webhook with id %q: %s", webhookID, handleLdapiErr(err)) + return diag.Errorf("failed to get webhook with id %q: %s", webhookID, handleLdapiErr(err)) } if webhook.Statements != nil { statements := policyStatementsToResourceData(*webhook.Statements) err = d.Set(STATEMENTS, statements) if err != nil { - return fmt.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) + return diag.Errorf("failed to set statements on webhook with id %q: %v", webhookID, err) } } @@ -61,7 +63,7 @@ func webhookRead(d *schema.ResourceData, meta interface{}, isDataSource bool) er err = d.Set(TAGS, webhook.Tags) if err != nil { - return fmt.Errorf("failed to set tags on webhook with id %q: %v", webhookID, err) + return diag.Errorf("failed to set tags on webhook with id %q: %v", webhookID, err) } - return nil + return diags } From 57f0c0f835d39ab4c2a09bb058b0c09a61ca3254 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Wed, 12 Jan 2022 12:47:03 +0100 Subject: [PATCH 23/36] add integration resource (#164) * stub integration resource * dump json of audit log configs to file * this should theoretically work * rename to launchdarkly_audit_log_subscription * add required to policy options - must be true for audit log subs * actually never mind i have decided on a better way to deal with it * set default statements and update tests * ehhh screw it we're making statements required since the api errors if not passed * ensure error if config is wrongly set * clean up comments * refactor for data source and add tests - not sure why failing * integration key must be required to get duh * finally fixed that * add resource doc * more docs * handle casing * handle bool types * update data source tests to use examples more likely to break * small docs update * update changelog * forgot to mention data source in changelog * add to the development doc * add strcase to go.mod * use const attributes for tests * update go client * forgot to update circle config * add pre commit to run python script for audit log sub configs * remove old handleRateLimit functions * udpate doc * bug fix * handle optional defaults * error if error reading json config file * use go struct * make on required * add examples * use context-aware functions * update doc to say on is required * define optional for data source * fix host issue * update doc * small refactor * add new line Co-authored-by: Fabian * return diag not nil from delete function * typo Co-authored-by: Fabian --- .circleci/config.yml | 3 + .pre-commit-config.yaml | 12 +- CHANGELOG.md | 6 +- docs/DEVELOPMENT.md | 2 + .../.terraform.lock.hcl | 23 ++ examples/v2/audit_log_subscription/example.tf | 60 ++++ go.mod | 3 +- go.sum | 6 +- .../audit_log_subscription_configs.go | 298 ++++++++++++++++++ .../audit_log_subscription_configs.json | 1 + launchdarkly/audit_log_subscription_helper.go | 252 +++++++++++++++ ...rce_launchdarkly_audit_log_subscription.go | 25 ++ ...aunchdarkly_audit_log_subscription_test.go | 116 +++++++ launchdarkly/keys.go | 1 + launchdarkly/policy_statements_helper.go | 7 +- launchdarkly/provider.go | 2 + ...rce_launchdarkly_audit_log_subscription.go | 139 ++++++++ ...aunchdarkly_audit_log_subscription_test.go | 263 ++++++++++++++++ .../generate_integration_audit_log_configs.py | 62 ++++ .../d/audit_log_subscription.html.markdown | 55 ++++ .../r/audit_log_subscription.html.markdown | 64 ++++ 21 files changed, 1393 insertions(+), 7 deletions(-) create mode 100644 examples/v2/audit_log_subscription/.terraform.lock.hcl create mode 100644 examples/v2/audit_log_subscription/example.tf create mode 100644 launchdarkly/audit_log_subscription_configs.go create mode 100644 launchdarkly/audit_log_subscription_configs.json create mode 100644 launchdarkly/audit_log_subscription_helper.go create mode 100644 launchdarkly/data_source_launchdarkly_audit_log_subscription.go create mode 100644 launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go create mode 100644 launchdarkly/resource_launchdarkly_audit_log_subscription.go create mode 100644 launchdarkly/resource_launchdarkly_audit_log_subscription_test.go create mode 100644 scripts/generate_integration_audit_log_configs.py create mode 100644 website/docs/d/audit_log_subscription.html.markdown create mode 100644 website/docs/r/audit_log_subscription.html.markdown diff --git a/.circleci/config.yml b/.circleci/config.yml index f609e11a..30294a84 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,6 +23,9 @@ jobs: - run: name: Test Access Token Resource command: TESTARGS="-run TestAccAccessToken" make testacc + - run: + name: Test Audit Log Subscription Resource + command: TESTARGS="-run TestAccAuditLogSubscription" make testacc - run: name: Test Custom Role Resource command: TESTARGS="-run TestAccCustomRole" make testacc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index caf53f4e..da478a0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,18 @@ repos: rev: v0.1.4 hooks: - id: gofmts - - repo: https://github.com/golangci/golangci-lint rev: v1.43.0 hooks: - id: golangci-lint + - repo: local + hooks: + - id: generate-audit-log-subscription-configs + name: Generate Audit Log Subscription Configurations + description: This hook runs a python script to update the audit log subscription configuration validation fields. + entry: python scripts/generate_integration_audit_log_configs.py + pass_filenames: false + language: python + additional_dependencies: ['requests'] + verbose: true + diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8506db..895cbef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## [2.4.0] (Unreleased) -ENHANCEMENTS: +FEATURES: + +- Added a new `launchdarkly_audit_log_subscription` resource and data source. + + ENHANCEMENTS: - Updated tests to use the constant attribute keys defined in launchdarkly/keys.go diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 97ba9d61..7d9a8f41 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -20,6 +20,8 @@ $ mkdir -p $HOME/development/terraform-providers/; cd $HOME/development/terrafor $ git clone git@github.com:launchdarkly/terraform-provider-launchdarkly ``` +If you are working on the `launchdarkly_audit_log_subscription` resource, you will want to ensure the configuration field mapping is up-to-date with the [most recent changes](https://github.com/launchdarkly/integration-framework/tree/master/integrations) while testing by `cd`-ing into `scripts/` and running `python generate_integration_audit_log_configs.py`. Please note you will need to have the Python `requests` package installed locally. Otherwise, this will be run as a git commit hook. Then, to update the go mapping, follow the instructions in audit_log_subscription_configs.go and commit and push your changes. + To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. ```sh diff --git a/examples/v2/audit_log_subscription/.terraform.lock.hcl b/examples/v2/audit_log_subscription/.terraform.lock.hcl new file mode 100644 index 00000000..2341cc74 --- /dev/null +++ b/examples/v2/audit_log_subscription/.terraform.lock.hcl @@ -0,0 +1,23 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/launchdarkly/launchdarkly" { + version = "2.3.0" + constraints = "~> 2.0" + hashes = [ + "h1:VR+DULiV82N4UpHtJun1r3JPP46kab89W/aHo2tt5OM=", + "zh:0af02f9cc42c6282d908b31ab3aa02754c0e9a8fd757a0e5a8a61da29de4cc68", + "zh:0ba4ba6351898598784005506a86ca60a08090e7ee30d3e465f0642ddf7ad830", + "zh:1d520405a977224077b72baf3e472b5092273af87ae265658f29100e4585ecec", + "zh:5b67f5fa15dbef0aff0c03cf5e00ee260c4665b35d23956c053a3e0f5ec62814", + "zh:6bbb63dc6db8a6e9591bd4855762706e1fbdc4308ea6256c8b1f76771aec46b9", + "zh:812fccb8d45e8edb237f2d1512be790cbccbfec650d36ab5fcf287ea71065fc1", + "zh:866b1596011d51319dbb95974319cc88099e912dcb4720e32c6e8442b45e4cee", + "zh:9ef51c7ff15633608c158b86c65cb37e7fdf455de571bafeb3e3134147bf4de7", + "zh:a0ae35202f11c1dc97b6e92aa6b9921016d7ba3022caa6eeba2ed731fb00a7bd", + "zh:a2700a77e5b8116a5c31ed338929fb6e13860656fd27681ef97b8efa1de1965b", + "zh:a56f16bcfab3185582c1e014507419d30154b0be03020d62f8fe9020bc326d1b", + "zh:d067c1d12728b4facda4affd77fbd644576491df218b8fe2cfcbdf690c0ecd55", + "zh:ee4a968d8b408126e9beee0bb215097894b365cdc683bf1e8a4e3a3cfb3b52bc", + ] +} diff --git a/examples/v2/audit_log_subscription/example.tf b/examples/v2/audit_log_subscription/example.tf new file mode 100644 index 00000000..c2e34f6e --- /dev/null +++ b/examples/v2/audit_log_subscription/example.tf @@ -0,0 +1,60 @@ +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } + required_version = ">= 0.13" +} + +resource "launchdarkly_audit_log_subscription" "datadog_example" { + integration_key = "datadog" + name = "Example Terraform Subscription" + config = { + api_key = "thisisasecretkey" + host_url = "https://api.datadoghq.com" + } + on = false + tags = ["terraform-managed"] + statements { + actions = ["*"] + effect = "deny" + resources = ["proj/*:env/*:flag/*"] + } +} + +resource "launchdarkly_audit_log_subscription" "dynatrace_example" { + integration_key = "dynatrace" + name = "Example Terraform Subscription" + config = { + api_token = "verysecrettoken" + url = "https://launchdarkly.appdynamics.com" + entity = "APPLICATION_METHOD" + } + tags = ["terraform-managed"] + on = true + statements { + actions = ["*"] + effect = "deny" + resources = ["proj/*:env/test:flag/*"] + } +} + +resource "launchdarkly_audit_log_subscription" "splunk_example" { + integration_key = "splunk" + name = "Example Terraform Subscription" + config = { + base_url = "https://launchdarkly.splunk.com" + token = "averysecrettoken" + skip_ca_verification = true + } + tags = ["terraform-managed"] + on = true + statements { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/production:flag/*"] + } +} + diff --git a/go.mod b/go.mod index 2c4d670f..1e31664e 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,12 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 // indirect github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect - github.com/launchdarkly/api-client-go/v7 v7.0.0 + github.com/launchdarkly/api-client-go/v7 v7.1.0 github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/stoewer/go-strcase v1.2.0 github.com/stretchr/testify v1.7.0 github.com/zclconf/go-cty v1.10.0 // indirect golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b // indirect diff --git a/go.sum b/go.sum index c306e80b..99b4d58c 100644 --- a/go.sum +++ b/go.sum @@ -273,8 +273,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/launchdarkly/api-client-go/v7 v7.0.0 h1:mCVGV3adts81Gtq2YxwCi6lvS/V9hYGJlqilLGFKj98= -github.com/launchdarkly/api-client-go/v7 v7.0.0/go.mod h1:5FlSAYTMrNa4UOiuSSL1+85NOiJel6cZT2P86ihNR9s= +github.com/launchdarkly/api-client-go/v7 v7.1.0 h1:A9+vYgtaM8fFmOn04qAGYvUH1OhnX8dGEZUWkKlE11g= +github.com/launchdarkly/api-client-go/v7 v7.1.0/go.mod h1:GVl1inKsWoKX3yLgdqrjxWw8k4ih0HlSmdnrhi5NNDs= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -332,6 +332,8 @@ github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/launchdarkly/audit_log_subscription_configs.go b/launchdarkly/audit_log_subscription_configs.go new file mode 100644 index 00000000..a20103e2 --- /dev/null +++ b/launchdarkly/audit_log_subscription_configs.go @@ -0,0 +1,298 @@ +package launchdarkly + +// to get the updated SUBSCRIPTION_CONFIGURATION_FIELDS value, paste the generated json in +// audit_log_subscription_configs.json into https://rodrigo-brito.github.io/json-to-go-map/ + +// TODO: generate this automatically +// func parseAuditLogSubscriptionConfigsFromJson() (map[string]IntegrationConfig, error) { +// var configs map[string]IntegrationConfig +// file, err := ioutil.ReadFile(CONFIG_FILE) +// if err != nil { +// return configs, err +// } + +// err = json.Unmarshal([]byte(file), &configs) +// if err != nil { +// return configs, err +// } +// return configs, nil +// } + +var SUBSCRIPTION_CONFIGURATION_FIELDS = map[string]interface{}{ + "appdynamics": map[string]interface{}{ + "account": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "applicationID": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + }, + "datadog": map[string]interface{}{ + "apiKey": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "hostURL": map[string]interface{}{ + "type": "enum", + "isOptional": true, + "allowedValues": []interface{}{ + "https://api.datadoghq.com", + "https://api.datadoghq.eu", + }, + "defaultValue": "https://api.datadoghq.com", + "isSecret": false, + }, + }, + "dynatrace": map[string]interface{}{ + "apiToken": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "url": map[string]interface{}{ + "type": "uri", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "entity": map[string]interface{}{ + "type": "enum", + "isOptional": true, + "allowedValues": []interface{}{ + "APPLICATION", + "APPLICATION_METHOD", + "APPLICATION_METHOD_GROUP", + "AUTO_SCALING_GROUP", + "AUXILIARY_SYNTHETIC_TEST", + "AWS_APPLICATION_LOAD_BALANCER", + "AWS_AVAILABILITY_ZONE", + "AWS_CREDENTIALS", + "AWS_LAMBDA_FUNCTION", + "AWS_NETWORK_LOAD_BALANCER", + "AZURE_API_MANAGEMENT_SERVICE", + "AZURE_APPLICATION_GATEWAY", + "AZURE_COSMOS_DB", + "AZURE_CREDENTIALS", + "AZURE_EVENT_HUB", + "AZURE_EVENT_HUB_NAMESPACE", + "AZURE_FUNCTION_APP", + "AZURE_IOT_HUB", + "AZURE_LOAD_BALANCER", + "AZURE_MGMT_GROUP", + "AZURE_REDIS_CACHE", + "AZURE_REGION", + "AZURE_SERVICE_BUS_NAMESPACE", + "AZURE_SERVICE_BUS_QUEUE", + "AZURE_SERVICE_BUS_TOPIC", + "AZURE_SQL_DATABASE", + "AZURE_SQL_ELASTIC_POOL", + "AZURE_SQL_SERVER", + "AZURE_STORAGE_ACCOUNT", + "AZURE_SUBSCRIPTION", + "AZURE_TENANT", + "AZURE_VM", + "AZURE_VM_SCALE_SET", + "AZURE_WEB_APP", + "CF_APPLICATION", + "CF_FOUNDATION", + "CINDER_VOLUME", + "CLOUD_APPLICATION", + "CLOUD_APPLICATION_INSTANCE", + "CLOUD_APPLICATION_NAMESPACE", + "CONTAINER_GROUP", + "CONTAINER_GROUP_INSTANCE", + "CUSTOM_APPLICATION", + "CUSTOM_DEVICE", + "CUSTOM_DEVICE_GROUP", + "DCRUM_APPLICATION", + "DCRUM_SERVICE", + "DCRUM_SERVICE_INSTANCE", + "DEVICE_APPLICATION_METHOD", + "DISK", + "DOCKER_CONTAINER_GROUP_INSTANCE", + "DYNAMO_DB_TABLE", + "EBS_VOLUME", + "EC2_INSTANCE", + "ELASTIC_LOAD_BALANCER", + "ENVIRONMENT", + "EXTERNAL_SYNTHETIC_TEST_STEP", + "GCP_ZONE", + "GEOLOCATION", + "GEOLOC_SITE", + "GOOGLE_COMPUTE_ENGINE", + "HOST", + "HOST_GROUP", + "HTTP_CHECK", + "HTTP_CHECK_STEP", + "HYPERVISOR", + "KUBERNETES_CLUSTER", + "KUBERNETES_NODE", + "MOBILE_APPLICATION", + "NETWORK_INTERFACE", + "NEUTRON_SUBNET", + "OPENSTACK_PROJECT", + "OPENSTACK_REGION", + "OPENSTACK_VM", + "OS", + "PROCESS_GROUP", + "PROCESS_GROUP_INSTANCE", + "RELATIONAL_DATABASE_SERVICE", + "SERVICE", + "SERVICE_INSTANCE", + "SERVICE_METHOD", + "SERVICE_METHOD_GROUP", + "SWIFT_CONTAINER", + "SYNTHETIC_LOCATION", + "SYNTHETIC_TEST", + "SYNTHETIC_TEST_STEP", + "VIRTUALMACHINE", + "VMWARE_DATACENTER", + }, + "defaultValue": "APPLICATION", + "isSecret": false, + }, + }, + "elastic": map[string]interface{}{ + "url": map[string]interface{}{ + "type": "uri", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "token": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "index": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + }, + "honeycomb": map[string]interface{}{ + "datasetName": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "apiKey": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + }, + "logdna": map[string]interface{}{ + "ingestionKey": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "level": map[string]interface{}{ + "type": "string", + "isOptional": true, + "allowedValues": nil, + "defaultValue": "INFO", + "isSecret": false, + }, + }, + "msteams": map[string]interface{}{ + "url": map[string]interface{}{ + "type": "uri", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + }, + "new-relic-apm": map[string]interface{}{ + "apiKey": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "applicationId": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "domain": map[string]interface{}{ + "type": "enum", + "isOptional": true, + "allowedValues": []interface{}{ + "api.newrelic.com", + "api.eu.newrelic.com", + }, + "defaultValue": "api.newrelic.com", + "isSecret": false, + }, + }, + "signalfx": map[string]interface{}{ + "accessToken": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "realm": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + }, + "splunk": map[string]interface{}{ + "base-url": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + "token": map[string]interface{}{ + "type": "string", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": true, + }, + "skip-ca-verification": map[string]interface{}{ + "type": "boolean", + "isOptional": false, + "allowedValues": nil, + "defaultValue": nil, + "isSecret": false, + }, + }, +} diff --git a/launchdarkly/audit_log_subscription_configs.json b/launchdarkly/audit_log_subscription_configs.json new file mode 100644 index 00000000..28a0add9 --- /dev/null +++ b/launchdarkly/audit_log_subscription_configs.json @@ -0,0 +1 @@ +{"appdynamics": {"account": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "applicationID": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "datadog": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "hostURL": {"type": "enum", "isOptional": true, "allowedValues": ["https://api.datadoghq.com", "https://api.datadoghq.eu"], "defaultValue": "https://api.datadoghq.com", "isSecret": false}}, "dynatrace": {"apiToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "entity": {"type": "enum", "isOptional": true, "allowedValues": ["APPLICATION", "APPLICATION_METHOD", "APPLICATION_METHOD_GROUP", "AUTO_SCALING_GROUP", "AUXILIARY_SYNTHETIC_TEST", "AWS_APPLICATION_LOAD_BALANCER", "AWS_AVAILABILITY_ZONE", "AWS_CREDENTIALS", "AWS_LAMBDA_FUNCTION", "AWS_NETWORK_LOAD_BALANCER", "AZURE_API_MANAGEMENT_SERVICE", "AZURE_APPLICATION_GATEWAY", "AZURE_COSMOS_DB", "AZURE_CREDENTIALS", "AZURE_EVENT_HUB", "AZURE_EVENT_HUB_NAMESPACE", "AZURE_FUNCTION_APP", "AZURE_IOT_HUB", "AZURE_LOAD_BALANCER", "AZURE_MGMT_GROUP", "AZURE_REDIS_CACHE", "AZURE_REGION", "AZURE_SERVICE_BUS_NAMESPACE", "AZURE_SERVICE_BUS_QUEUE", "AZURE_SERVICE_BUS_TOPIC", "AZURE_SQL_DATABASE", "AZURE_SQL_ELASTIC_POOL", "AZURE_SQL_SERVER", "AZURE_STORAGE_ACCOUNT", "AZURE_SUBSCRIPTION", "AZURE_TENANT", "AZURE_VM", "AZURE_VM_SCALE_SET", "AZURE_WEB_APP", "CF_APPLICATION", "CF_FOUNDATION", "CINDER_VOLUME", "CLOUD_APPLICATION", "CLOUD_APPLICATION_INSTANCE", "CLOUD_APPLICATION_NAMESPACE", "CONTAINER_GROUP", "CONTAINER_GROUP_INSTANCE", "CUSTOM_APPLICATION", "CUSTOM_DEVICE", "CUSTOM_DEVICE_GROUP", "DCRUM_APPLICATION", "DCRUM_SERVICE", "DCRUM_SERVICE_INSTANCE", "DEVICE_APPLICATION_METHOD", "DISK", "DOCKER_CONTAINER_GROUP_INSTANCE", "DYNAMO_DB_TABLE", "EBS_VOLUME", "EC2_INSTANCE", "ELASTIC_LOAD_BALANCER", "ENVIRONMENT", "EXTERNAL_SYNTHETIC_TEST_STEP", "GCP_ZONE", "GEOLOCATION", "GEOLOC_SITE", "GOOGLE_COMPUTE_ENGINE", "HOST", "HOST_GROUP", "HTTP_CHECK", "HTTP_CHECK_STEP", "HYPERVISOR", "KUBERNETES_CLUSTER", "KUBERNETES_NODE", "MOBILE_APPLICATION", "NETWORK_INTERFACE", "NEUTRON_SUBNET", "OPENSTACK_PROJECT", "OPENSTACK_REGION", "OPENSTACK_VM", "OS", "PROCESS_GROUP", "PROCESS_GROUP_INSTANCE", "RELATIONAL_DATABASE_SERVICE", "SERVICE", "SERVICE_INSTANCE", "SERVICE_METHOD", "SERVICE_METHOD_GROUP", "SWIFT_CONTAINER", "SYNTHETIC_LOCATION", "SYNTHETIC_TEST", "SYNTHETIC_TEST_STEP", "VIRTUALMACHINE", "VMWARE_DATACENTER"], "defaultValue": "APPLICATION", "isSecret": false}}, "elastic": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "index": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "honeycomb": {"datasetName": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}}, "logdna": {"ingestionKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "level": {"type": "string", "isOptional": true, "allowedValues": null, "defaultValue": "INFO", "isSecret": false}}, "msteams": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "new-relic-apm": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "applicationId": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "domain": {"type": "enum", "isOptional": true, "allowedValues": ["api.newrelic.com", "api.eu.newrelic.com"], "defaultValue": "api.newrelic.com", "isSecret": false}}, "signalfx": {"accessToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "realm": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "splunk": {"base-url": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "skip-ca-verification": {"type": "boolean", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}} \ No newline at end of file diff --git a/launchdarkly/audit_log_subscription_helper.go b/launchdarkly/audit_log_subscription_helper.go new file mode 100644 index 00000000..f5dfe3c9 --- /dev/null +++ b/launchdarkly/audit_log_subscription_helper.go @@ -0,0 +1,252 @@ +package launchdarkly + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + strcase "github.com/stoewer/go-strcase" +) + +var KEBAB_CASE_INTEGRATIONS = []string{"splunk"} + +type IntegrationConfig map[string]FormVariable + +type FormVariable struct { + Type string + IsOptional *bool + AllowedValues *[]string + DefaultValue *interface{} + IsSecret *bool +} + +func auditLogSubscriptionSchema(isDataSource bool) map[string]*schema.Schema { + return map[string]*schema.Schema{ + INTEGRATION_KEY: { + // validated as part of the config validation + Type: schema.TypeString, + Required: true, + // we are omitting appdynamics for now because it requires oauth + ValidateFunc: validation.StringNotInSlice([]string{"appdynamics"}, false), + }, + NAME: { + Type: schema.TypeString, + Required: !isDataSource, + Optional: isDataSource, + }, + CONFIG: { + Type: schema.TypeMap, + Required: !isDataSource, + Optional: isDataSource, + }, + STATEMENTS: policyStatementsSchema(policyStatementSchemaOptions{required: !isDataSource}), + ON: { + Type: schema.TypeBool, + Required: !isDataSource, + Optional: isDataSource, + }, + TAGS: tagsSchema(), + } +} + +func parseAuditLogSubscriptionConfigs() map[string]IntegrationConfig { + // SUBSCRIPTION_CONFIGURATION_FIELDS can be found in audit_log_subscription_configs.go + configs := make(map[string]IntegrationConfig, len(SUBSCRIPTION_CONFIGURATION_FIELDS)) + for integrationKey, rawVariables := range SUBSCRIPTION_CONFIGURATION_FIELDS { + cfg := IntegrationConfig{} + variables := rawVariables.(map[string]interface{}) + for k, v := range variables { + variable := v.(map[string]interface{}) + formVariable := FormVariable{Type: variable["type"].(string)} + if variable["isOptional"] != nil { + isOptional := variable["isOptional"].(bool) + formVariable.IsOptional = &isOptional + } + if variable["allowedValues"] != nil { + rawValues := variable["allowedValues"].([]interface{}) + var allowedValues []string + for _, value := range rawValues { + allowedValues = append(allowedValues, value.(string)) + } + formVariable.AllowedValues = &allowedValues + } + if variable["isSecret"] != nil { + isSecret := variable["isSecret"].(bool) + formVariable.IsSecret = &isSecret + } + if variable["defaultValue"] != nil { + defaultValue := variable["defaultValue"] + formVariable.DefaultValue = &defaultValue + } + cfg[k] = formVariable + } + configs[integrationKey] = cfg + } + return configs +} + +func getConfigFieldKey(integrationKey, resourceKey string) string { + // a select number of integrations take fields in kebab case, ex. "skip-ca-verification" + // currently this only applies to splunk + for _, integration := range KEBAB_CASE_INTEGRATIONS { + if integrationKey == integration { + return strcase.KebabCase(resourceKey) + } + } + return strcase.LowerCamelCase(resourceKey) +} + +// configFromResourceData uses the configuration generated into audit_log_subscription_config.json +// to validate and generate the config the API expects +func configFromResourceData(d *schema.ResourceData) (map[string]interface{}, error) { + // TODO: refactor to return list of diags warnings with all formatting errors + integrationKey := d.Get(INTEGRATION_KEY).(string) + config := d.Get(CONFIG).(map[string]interface{}) + configMap := parseAuditLogSubscriptionConfigs() + configFormat, ok := configMap[integrationKey] + if !ok { + return config, fmt.Errorf("%s is not a valid integration_key for audit log subscriptions", integrationKey) + } + for k := range config { + // error if an incorrect config variable has been set + key := getConfigFieldKey(integrationKey, k) // convert casing to compare to required config format + if integrationKey == "datadog" && key == "hostUrl" { + // this is a one-off for now + key = "hostURL" + } + if _, ok := configFormat[key]; !ok { + return config, fmt.Errorf("config variable %s not valid for integration type %s", k, integrationKey) + } + } + convertedConfig := make(map[string]interface{}, len(config)) + for k, v := range configFormat { + key := strcase.SnakeCase(k) // convert to snake case to validate user config + rawValue, ok := config[key] + if !ok { + if !*v.IsOptional { + return config, fmt.Errorf("config variable %s must be set", key) + } + // we will let the API handle default configs for now since it otherwise messes + // up the plan if we set an attribute a user has not set on a non-computed attribute + continue + } + // type will be one of ["string", "boolean", "uri", "enum", "oauth", "dynamicEnum"] + // for now we do not need to handle oauth or dynamicEnum + switch v.Type { + case "string", "uri": + // we'll let the API handle the URI validation for now + value := rawValue.(string) + convertedConfig[k] = value + case "boolean": + value, err := strconv.ParseBool(rawValue.(string)) // map values may only be one type, so all non-string types have to be converted + if err != nil { + return config, fmt.Errorf("config value %s for %v must be of type bool", rawValue, k) + } + convertedConfig[k] = value + case "enum": + value := rawValue.(string) + if !stringInSlice(value, *v.AllowedValues) { + return config, fmt.Errorf("config value %s for %v must be one of the following approved string values: %v", rawValue, k, *v.AllowedValues) + } + convertedConfig[k] = value + default: + // just set to the existing value + convertedConfig[k] = rawValue + } + } + return convertedConfig, nil +} + +func configToResourceData(d *schema.ResourceData, config map[string]interface{}) (map[string]interface{}, error) { + integrationKey := d.Get(INTEGRATION_KEY).(string) + configMap := parseAuditLogSubscriptionConfigs() + configFormat, ok := configMap[integrationKey] + if !ok { + return config, fmt.Errorf("%s is not a currently supported integration_key for audit log subscriptions", integrationKey) + } + originalConfig := d.Get(CONFIG).(map[string]interface{}) + convertedConfig := make(map[string]interface{}, len(config)) + for k, v := range config { + key := strcase.SnakeCase(k) + // some attributes have defaults that the API will return and terraform will complain since config + // is not a computed attribute (cannot be both required & computed) + // TODO: handle this in a SuppressDiff function + if _, setByUser := originalConfig[key]; !setByUser { + continue + } + convertedConfig[key] = v + if value, isBool := v.(bool); isBool { + convertedConfig[key] = strconv.FormatBool(value) + } + if *configFormat[k].IsSecret { + // if the user didn't put it in as obfuscated, we don't want to set it as obfuscated + convertedConfig[key] = originalConfig[key] + } + } + return convertedConfig, nil +} + +func auditLogSubscriptionRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) + var id string + if isDataSource { + id = d.Get(ID).(string) + } else { + id = d.Id() + } + integrationKey := d.Get(INTEGRATION_KEY).(string) + + sub, res, err := client.ld.IntegrationAuditLogSubscriptionsApi.GetSubscriptionByID(client.ctx, integrationKey, id).Execute() + + if isStatusNotFound(res) && !isDataSource { + log.Printf("[WARN] failed to find integration with ID %q, removing from state if present", id) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find integration with ID %q, removing from state if present", id), + }) + d.SetId("") + return diags + } + if err != nil { + return diag.Errorf("failed to get integration with ID %q: %v", id, err) + } + + if isDataSource { + d.SetId(*sub.Id) + } + + _ = d.Set(INTEGRATION_KEY, sub.Kind) + _ = d.Set(NAME, sub.Name) + _ = d.Set(ON, sub.On) + cfg, err := configToResourceData(d, *sub.Config) + if err != nil { + return diag.Errorf("failed to set config on integration with id %q: %v", *sub.Id, err) + } + err = d.Set(CONFIG, cfg) + if err != nil { + return diag.Errorf("failed to set config on integration with id %q: %v", *sub.Id, err) + } + err = d.Set(STATEMENTS, policyStatementsToResourceData(*sub.Statements)) + if err != nil { + return diag.Errorf("failed to set statements on integration with id %q: %v", *sub.Id, err) + } + err = d.Set(TAGS, sub.Tags) + if err != nil { + return diag.Errorf("failed to set tags on integration with id %q: %v", *sub.Id, err) + } + return diags +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} diff --git a/launchdarkly/data_source_launchdarkly_audit_log_subscription.go b/launchdarkly/data_source_launchdarkly_audit_log_subscription.go new file mode 100644 index 00000000..be4e7bfa --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_audit_log_subscription.go @@ -0,0 +1,25 @@ +package launchdarkly + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAuditLogSubscription() *schema.Resource { + schemaMap := auditLogSubscriptionSchema(true) + schemaMap[ID] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The audit log subscription ID", + } + return &schema.Resource{ + ReadContext: dataSourceAuditLogSubscriptionRead, + Schema: schemaMap, + } +} + +func dataSourceAuditLogSubscriptionRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return auditLogSubscriptionRead(ctx, d, metaRaw, true) +} diff --git a/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go b/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go new file mode 100644 index 00000000..5337d5d7 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go @@ -0,0 +1,116 @@ +package launchdarkly + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + ldapi "github.com/launchdarkly/api-client-go/v7" + "github.com/stretchr/testify/require" +) + +const ( + testAccDataSourceAuditLogSubscriptionBasic = ` +data "launchdarkly_audit_log_subscription" "test" { + id = "%s" + integration_key = "%s" +} +` + + testAccDataSourceAuditLogSubscriptionExists = ` +data "launchdarkly_audit_log_subscription" "test" { + id = "%s" + integration_key = "%s" + } + ` +) + +func testAccDataSourceAuditLogSubscriptionCreate(client *Client, integrationKey string, subscriptionBody ldapi.SubscriptionPost) (*ldapi.Integration, error) { + statements := []ldapi.StatementPost{{ + Effect: "allow", + Resources: []string{"proj/*"}, + Actions: []string{"*"}, + }} + subscriptionBody.Statements = &statements + + sub, _, err := client.ld.IntegrationAuditLogSubscriptionsApi.CreateSubscription(client.ctx, integrationKey).SubscriptionPost(subscriptionBody).Execute() + if err != nil { + return nil, fmt.Errorf("failed to create integration subscription for test: %v", handleLdapiErr(err)) + } + return &sub, nil +} + +func testAccDataSourceAuditLogSubscriptionDelete(client *Client, integrationKey, id string) error { + _, err := client.ld.IntegrationAuditLogSubscriptionsApi.DeleteSubscription(client.ctx, integrationKey, id).Execute() + + if err != nil { + return fmt.Errorf("failed to delete integration with ID %q: %s", id, handleLdapiErr(err)) + } + return nil +} + +func TestAccDataSourceAuditLogSubscription_noMatchReturnsError(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + id := "fake-id" + integrationKey := "msteams" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceAuditLogSubscriptionBasic, id, integrationKey), + ExpectError: regexp.MustCompile(`Error: failed to get integration with ID "fake-id": 404 Not Found`), + }, + }, + }) +} + +func TestAccDataSourceAuditLogSubscription_exists(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + + integrationKey := "datadog" + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + + subscriptionBody := ldapi.SubscriptionPost{ + Name: "test subscription", + Config: map[string]interface{}{ + "apiKey": "thisisasecretkey", + "hostURL": "https://api.datadoghq.com", + }, + } + sub, err := testAccDataSourceAuditLogSubscriptionCreate(client, integrationKey, subscriptionBody) + require.NoError(t, err) + + defer func() { + err := testAccDataSourceAuditLogSubscriptionDelete(client, integrationKey, *sub.Id) + require.NoError(t, err) + }() + + resourceName := "data.launchdarkly_audit_log_subscription.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceAuditLogSubscriptionExists, *sub.Id, integrationKey), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "id", *sub.Id), + ), + }, + }, + }) +} diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 564918db..8a151f2c 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -44,6 +44,7 @@ const ( INCLUDED = "included" INCLUDE_IN_SNIPPET = "include_in_snippet" INLINE_ROLES = "inline_roles" + INTEGRATION_KEY = "integration_key" KEY = "key" KIND = "kind" LAST_NAME = "last_name" diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 4424211d..1aa24f99 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -15,16 +15,18 @@ type policyStatementSchemaOptions struct { deprecated string description string conflictsWith []string + required bool } func policyStatementsSchema(options policyStatementSchemaOptions) *schema.Schema { - return &schema.Schema{ + schema := &schema.Schema{ Type: schema.TypeList, - Optional: true, MinItems: 1, Description: options.description, Deprecated: options.deprecated, ConflictsWith: options.conflictsWith, + Optional: !options.required, + Required: options.required, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ RESOURCES: { @@ -71,6 +73,7 @@ func policyStatementsSchema(options policyStatementSchemaOptions) *schema.Schema }, }, } + return schema } func validatePolicyStatement(statement map[string]interface{}) error { diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 9cf1b6d7..4ac0f3c0 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -56,6 +56,7 @@ func Provider() *schema.Provider { "launchdarkly_feature_flag_environment": resourceFeatureFlagEnvironment(), "launchdarkly_destination": resourceDestination(), "launchdarkly_access_token": resourceAccessToken(), + "launchdarkly_audit_log_subscription": resourceAuditLogSubscription(), }, DataSourcesMap: map[string]*schema.Resource{ "launchdarkly_team_member": dataSourceTeamMember(), @@ -65,6 +66,7 @@ func Provider() *schema.Provider { "launchdarkly_feature_flag_environment": dataSourceFeatureFlagEnvironment(), "launchdarkly_webhook": dataSourceWebhook(), "launchdarkly_segment": dataSourceSegment(), + "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), }, ConfigureFunc: providerConfigure, } diff --git a/launchdarkly/resource_launchdarkly_audit_log_subscription.go b/launchdarkly/resource_launchdarkly_audit_log_subscription.go new file mode 100644 index 00000000..b2da548f --- /dev/null +++ b/launchdarkly/resource_launchdarkly_audit_log_subscription.go @@ -0,0 +1,139 @@ +package launchdarkly + +import ( + "context" + "fmt" + "log" + "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/v7" +) + +func resourceAuditLogSubscription() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAuditLogSubscriptionCreate, + UpdateContext: resourceAuditLogSubscriptionUpdate, + DeleteContext: resourceAuditLogSubscriptionDelete, + ReadContext: resourceAuditLogSubscriptionRead, + Exists: resourceAuditLogSubscriptionExists, + + Importer: &schema.ResourceImporter{ + State: resourceAuditLogSubscriptionImport, + }, + + Schema: auditLogSubscriptionSchema(false), + } +} + +func resourceAuditLogSubscriptionCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + integrationKey := d.Get(INTEGRATION_KEY).(string) + name := d.Get(NAME).(string) + on := d.Get(ON).(bool) + tags := stringsFromSchemaSet(d.Get(TAGS).(*schema.Set)) + config, err := configFromResourceData(d) + if err != nil { + return diag.Errorf("failed to create %s integration with name %s: %v", integrationKey, name, err.Error()) + } + + statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) + if err != nil { + return diag.Errorf("failed to create %s integration with name %s: %v", integrationKey, name, err.Error()) + } + + subscriptionBody := ldapi.SubscriptionPost{ + Name: name, + On: &on, + Tags: &tags, + Config: config, + Statements: &statements, + } + + sub, _, err := client.ld.IntegrationAuditLogSubscriptionsApi.CreateSubscription(client.ctx, integrationKey).SubscriptionPost(subscriptionBody).Execute() + + if err != nil { + return diag.Errorf("failed to create %s integration with name %s: %v", integrationKey, name, handleLdapiErr(err)) + } + d.SetId(*sub.Id) + return resourceAuditLogSubscriptionRead(ctx, d, metaRaw) +} + +func resourceAuditLogSubscriptionUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + integrationKey := d.Get(INTEGRATION_KEY).(string) + name := d.Get(NAME).(string) + tags := stringsFromResourceData(d, TAGS) + on := d.Get(ON).(bool) + config, err := configFromResourceData(d) + if err != nil { + return diag.FromErr(err) + } + id := d.Id() + + statements, err := policyStatementsFromResourceData(d.Get(STATEMENTS).([]interface{})) + if err != nil { + return diag.FromErr(err) + } + + patch := []ldapi.PatchOperation{ + patchReplace("/name", &name), + patchReplace("/tags", &tags), + patchReplace("/config", &config), + patchReplace("/on", &on), + patchReplace("/statements", &statements), + } + + _, _, err = client.ld.IntegrationAuditLogSubscriptionsApi.UpdateSubscription(client.ctx, integrationKey, id).PatchOperation(patch).Execute() + if err != nil { + return diag.Errorf("failed to update %q integration with name %q and ID %q: %s", integrationKey, name, id, handleLdapiErr(err)) + } + return resourceAuditLogSubscriptionRead(ctx, d, metaRaw) +} + +func resourceAuditLogSubscriptionDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + id := d.Id() + integrationKey := d.Get(INTEGRATION_KEY).(string) + + _, err := client.ld.IntegrationAuditLogSubscriptionsApi.DeleteSubscription(client.ctx, integrationKey, id).Execute() + + if err != nil { + return diag.Errorf("failed to delete integration with ID %q: %s", id, handleLdapiErr(err)) + } + return diag.Diagnostics{} +} + +func resourceAuditLogSubscriptionRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return auditLogSubscriptionRead(ctx, d, metaRaw, false) +} + +func resourceAuditLogSubscriptionExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { + client := metaRaw.(*Client) + id := d.Id() + integrationKey := d.Get(INTEGRATION_KEY).(string) + + _, res, err := client.ld.IntegrationAuditLogSubscriptionsApi.GetSubscriptionByID(client.ctx, integrationKey, id).Execute() + if isStatusNotFound(res) { + log.Println("got 404 when getting integration. returning false.") + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to get integration with ID %q: %v", id, handleLdapiErr(err)) + } + return true, nil +} + +func resourceAuditLogSubscriptionImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + id := d.Id() + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return nil, fmt.Errorf("found unexpected id format for import: %q. expected format: 'integrationKey/integration_id'", id) + } + + integrationKey, integrationID := parts[0], parts[1] + _ = d.Set(INTEGRATION_KEY, integrationKey) + d.SetId(integrationID) + return []*schema.ResourceData{d}, nil +} diff --git a/launchdarkly/resource_launchdarkly_audit_log_subscription_test.go b/launchdarkly/resource_launchdarkly_audit_log_subscription_test.go new file mode 100644 index 00000000..dbdb4a2b --- /dev/null +++ b/launchdarkly/resource_launchdarkly_audit_log_subscription_test.go @@ -0,0 +1,263 @@ +package launchdarkly + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + testAccAuditLogSubscriptionCreate = ` +resource "launchdarkly_audit_log_subscription" "%s_tf_test" { + integration_key = "%s" + name = "terraform test" + config = %s + tags = [ + "integrations", + "terraform" + ] + on = true + statements { + actions = ["*"] + effect = "deny" + resources = ["proj/*:env/*:flag/*"] + } +} +` + + testAccAuditLogSubscriptionUpdate = ` +resource "launchdarkly_audit_log_subscription" "%s_tf_test" { + integration_key = "%s" + name = "terraform test v2" + config = %s + on = false + tags = [ + "integrations" + ] + statements { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/production"] + } +} +` +) + +func TestAccAuditLogSubscription_CreateUpdateDatadog(t *testing.T) { + integrationKey := "datadog" + // omitting host_url = "https://api.datadoghq.com" to test the handling of attributes with default values + config := `{ + api_key = "thisisasecretkey" + } + ` + + resourceName := fmt.Sprintf("launchdarkly_audit_log_subscription.%s_tf_test", integrationKey) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionCreate, integrationKey, integrationKey, config), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, integrationKey), + resource.TestCheckResourceAttr(resourceName, NAME, "terraform test"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), + resource.TestCheckResourceAttr(resourceName, "config.api_key", "thisisasecretkey"), + // resource.TestCheckResourceAttr(resourceName, "config.host_url", "https://api.datadoghq.com"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "integrations"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/*:flag/*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "deny"), + ), + }, + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionUpdate, integrationKey, integrationKey, config), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, integrationKey), + resource.TestCheckResourceAttr(resourceName, NAME, "terraform test v2"), + resource.TestCheckResourceAttr(resourceName, ON, "false"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "integrations"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/production"), + ), + }, + }, + }) +} + +func TestAccAuditLogSubscription_CreateDynatrace(t *testing.T) { + integrationKey := "dynatrace" + config := `{ + api_token = "verysecrettoken" + url = "https://launchdarkly.appdynamics.com" + entity = "APPLICATION_METHOD" + } + ` + + resourceName := fmt.Sprintf("launchdarkly_audit_log_subscription.%s_tf_test", integrationKey) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionCreate, integrationKey, integrationKey, config), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, integrationKey), + resource.TestCheckResourceAttr(resourceName, NAME, "terraform test"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), + resource.TestCheckResourceAttr(resourceName, "config.api_token", "verysecrettoken"), + resource.TestCheckResourceAttr(resourceName, "config.url", "https://launchdarkly.appdynamics.com"), + resource.TestCheckResourceAttr(resourceName, "config.entity", "APPLICATION_METHOD"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "integrations"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/*:flag/*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "deny"), + ), + }, + }, + }) +} + +func TestAccAuditLogSubscription_CreateMSTeams(t *testing.T) { + integrationKey := "msteams" + config := `{ + url = "https://outlook.office.com/webhook/terraform-test" + } + ` + + resourceName := fmt.Sprintf("launchdarkly_audit_log_subscription.%s_tf_test", integrationKey) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionCreate, integrationKey, integrationKey, config), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, integrationKey), + resource.TestCheckResourceAttr(resourceName, NAME, "terraform test"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), + resource.TestCheckResourceAttr(resourceName, "config.url", "https://outlook.office.com/webhook/terraform-test"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "integrations"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/*:flag/*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "deny"), + ), + }, + }, + }) +} + +func TestAccAuditLogSubscription_CreateSplunk(t *testing.T) { + // splunk specifically needs to be converted to kebab case, so we need to handle it specially + integrationKey := "splunk" + config := `{ + base_url = "https://launchdarkly.splunk.com" + token = "averysecrettoken" + skip_ca_verification = true + } + ` + + resourceName := fmt.Sprintf("launchdarkly_audit_log_subscription.%s_tf_test", integrationKey) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionCreate, integrationKey, integrationKey, config), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, ID), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, integrationKey), + resource.TestCheckResourceAttr(resourceName, NAME, "terraform test"), + resource.TestCheckResourceAttr(resourceName, ON, "true"), + resource.TestCheckResourceAttr(resourceName, "config.base_url", "https://launchdarkly.splunk.com"), + resource.TestCheckResourceAttr(resourceName, "config.token", "averysecrettoken"), + resource.TestCheckResourceAttr(resourceName, "config.skip_ca_verification", "true"), + resource.TestCheckResourceAttr(resourceName, "tags.#", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "integrations"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "terraform"), + resource.TestCheckResourceAttr(resourceName, "statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "statements.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.resources.0", "proj/*:env/*:flag/*"), + resource.TestCheckResourceAttr(resourceName, "statements.0.effect", "deny"), + ), + }, + }, + }) +} + +func TestAccAuditLogSubscription_WrongConfigReturnsError(t *testing.T) { + integrationKey := "honeycomb" + config := `{ + url = "https://bad-config.com/terraform-test" + } + ` + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccAuditLogSubscriptionCreate, integrationKey, integrationKey, config), + ExpectError: regexp.MustCompile(`Error: failed to create honeycomb integration with name terraform test: config variable url not valid for integration type honeycomb`), + }, + }, + }) +} + +func testAccCheckIntegrationExists(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) + } + integrationKey, ok := rs.Primary.Attributes[INTEGRATION_KEY] + if !ok { + return fmt.Errorf("integration integrationKey not found: %s", resourceName) + } + integrationID, ok := rs.Primary.Attributes[ID] + if !ok { + return fmt.Errorf("integration not found: %s", resourceName) + } + client := testAccProvider.Meta().(*Client) + _, _, err := client.ld.IntegrationAuditLogSubscriptionsApi.GetSubscriptionByID(client.ctx, integrationKey, integrationID).Execute() + if err != nil { + return fmt.Errorf("error getting %s integration: %s", integrationKey, err) + } + + return nil + } +} diff --git a/scripts/generate_integration_audit_log_configs.py b/scripts/generate_integration_audit_log_configs.py new file mode 100644 index 00000000..6aa32f8a --- /dev/null +++ b/scripts/generate_integration_audit_log_configs.py @@ -0,0 +1,62 @@ +import os +import requests +import json + +def get_audit_log_manifests(host, api_key): + if not host or not api_key: + raise Exception('host or api key not set') + path_get_manifests = '/api/v2/integration-manifests' + resp = requests.get(host + path_get_manifests, headers={'Authorization': api_key}) + if resp.status_code != 200: + raise Exception(resp.status_code, 'unsuccessful get manifests request') + return filter_manifests(resp.json()['items']) + +def filter_manifests(manifests): + filtered = [] + for m in manifests: + if 'capabilities' in m and 'auditLogEventsHook' in m['capabilities']: + filtered.append(m) + return filtered + +def construct_config(manifest): + """ takes an audit log manifest and returns the form variables in the format + { : { + 'type': , + 'isOptional': , + 'allowedValues': , + 'defaultValue': , + 'isSecret': + } } + """ + rawFormVariables = manifest['formVariables'] + formVariables = {} + for rawV in rawFormVariables: + v = { 'type': rawV['type'] } + for attribute in ['isOptional', 'allowedValues', 'defaultValue', 'isSecret']: + if attribute in rawV: + v[attribute] = rawV[attribute] + formVariables[rawV['key']] = v + return formVariables + +def construct_config_dict(manifests): + cfgs = {} + for m in manifests: + cfgs[m['key']] = construct_config(m) + return cfgs + +def seed_config_file(): + host = os.getenv('LAUNCHDARKLY_API_HOST', 'https://app.launchdarkly.com') + if not host.startswith('http'): + host = 'https://' + host + api_key = os.getenv('LAUNCHDARKLY_ACCESS_TOKEN') + print('getting manifests...') + manifests = get_audit_log_manifests(host, api_key) + print('constructing configs...') + configs = construct_config_dict(manifests) + print('seeding file...') + with open('launchdarkly/audit_log_subscription_configs.json', 'w') as f: + json.dump(configs, f) + print('COMPLETE, config data written to launchdarkly/audit_log_subscription_configs.json') + +if __name__ == '__main__': + seed_config_file() \ No newline at end of file diff --git a/website/docs/d/audit_log_subscription.html.markdown b/website/docs/d/audit_log_subscription.html.markdown new file mode 100644 index 00000000..a7668ab2 --- /dev/null +++ b/website/docs/d/audit_log_subscription.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "launchdarkly" +page_title: "LaunchDarkly: launchdarkly_audit_log_subscription" +description: |- + Get information about LaunchDarkly audit log subscriptions. +--- + +# launchdarkly_audit_log_subscription + +Provides a LaunchDarkly audit log subscription data source. + +This data source allows you to retrieve information about LaunchDarkly audit log subscriptions. + +# Example Usage + +```hcl +data "launchdarkly_audit_log_subscription" "test" { + id = "5f0cd446a77cba0b4c5644a7" + integration_key = "msteams" +} +``` + +## Argument Reference + +- `id` (Required) - The unique subscription ID. This can be found in the URL of the pull-out configuration sidebar for the given subscription on your [LaunchDarkly Integrations page](https://app.launchdarkly.com/default/integrations). + +- `integration_key` (Required) - The integration key. As of January 2022, supported integrations are `"datadog"`, `"dynatrace"`, `"elastic"`, `"honeycomb"`, `"logdna"`, `"msteams"`, `"new-relic-apm"`, `"signalfx"`, and `"splunk"`. + +## Attributes Reference + +In addition to the arguments above, the resource exports following attributes: + +- `name` - The subscription's human-readable name. + +- `config` - A block of configuration fields associated with your integration type. + +- `statements` - The statement block used to filter subscription events. To learn more, read [Statement Blocks](#statement-blocks). + +- `on` - Whether the subscription is enabled. + +- `tags` - Set of tags associated with the subscription. + +### Statement Blocks + +Audit log subscription `statements` blocks are composed of the following arguments: + +- `effect` - Either `allow` or `deny`. This argument defines whether the statement allows or denies access to the named resources and actions. + +- `resources` - The list of resource specifiers defining the resources to which the statement applies. To learn more about how to configure these read [Using resources](https://docs.launchdarkly.com/home/members/role-resources). + +- `not_resources` - The list of resource specifiers defining the resources to which the statement does not apply. To learn more about how to configure these, read [Using resources](https://docs.launchdarkly.com/home/members/role-resources). + +- `actions` The list of action specifiers defining the actions to which the statement applies. For a list of available actions, read [Using actions](https://docs.launchdarkly.com/home/members/role-actions). + +- `not_actions` The list of action specifiers defining the actions to which the statement does not apply. For a list of available actions, read [Using actions](https://docs.launchdarkly.com/home/members/role-actions). diff --git a/website/docs/r/audit_log_subscription.html.markdown b/website/docs/r/audit_log_subscription.html.markdown new file mode 100644 index 00000000..c0f6928c --- /dev/null +++ b/website/docs/r/audit_log_subscription.html.markdown @@ -0,0 +1,64 @@ +--- +layout: "launchdarkly" +page_title: "LaunchDarkly: launchdarkly_audit_log_subscription" +description: |- + Create and manage LaunchDarkly integration audit log subscriptions. +--- + +# launchdarkly_audit_log_subscription + +Provides a LaunchDarkly audit log subscription resource. + +This resource allows you to create and manage LaunchDarkly audit log subscriptions. + +# Example Usage + +```hcl +resource "launchdarkly_audit_log_subscription" "example" { + integration_key = "datadog" + name = "Example Datadog Subscription" + config { + api_key = "yoursecretkey" + host_url = "https://api.datadoghq.com" + } + tags = [ + "integrations", + "terraform" + ] + statements { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/*:flag/*"] + } +} +``` + +## Argument Reference + +- `integration_key` (Required) The integration key. As of January 2022, supported integrations are `"datadog"`, `"dynatrace"`, `"elastic"`, `"honeycomb"`, `"logdna"`, `"msteams"`, `"new-relic-apm"`, `"signalfx"`, and `"splunk"`. + +- `name` (Required) - A human-friendly name for your audit log subscription viewable from within the LaunchDarkly Integrations page. + +- `config` (Required) - The set of configuration fields corresponding to the value defined for `integration_key`. Refer to the `"formVariables"` field in the corresponding `integrations//manifest.json` file in [this repo](https://github.com/launchdarkly/integration-framework/tree/master/integrations) for a full list of fields for the integration you wish to configure. **IMPORTANT**: Please note that Terraform will only accept these in snake case, regardless of the case shown in the manifest. + +- `statements` (Required) - A block representing the resources to which you wish to subscribe. To learn more about how to configure these blocks, read [Nested Subscription Statements Blocks](#nested-subscription-statements-blocks). + +- `on` (Required) - Whether or not you want your subscription enabled, i.e. to actively send events. + +- `tags` (Optional) - Set of tags associated with the subscription object. + +### Nested Subscription Statements Blocks + +Nested subscription `statements` blocks have the following structure: + +- `effect` (Required) - Either `allow` or `deny`. This argument defines whether the statement allows or denies access to the named resources and actions. + +- `resources` - The list of resource specifiers defining the resources to which the statement applies. To learn more about how to configure these, read [Using resources](https://docs.launchdarkly.com/home/members/role-resources). + +- `not_resources` - The list of resource specifiers defining the resources to which the statement does not apply. To learn more about how to configure these, read [Using resources](https://docs.launchdarkly.com/home/members/role-resources). + +- `actions` The list of action specifiers defining the actions to which the statement applies. For a list of available actions, read [Using actions](https://docs.launchdarkly.com/home/members/role-actions). + +- `not_actions` The list of action specifiers defining the actions to which the statement does not apply. For a list of available actions, read [Using actions](https://docs.launchdarkly.com/home/members/role-actions). + +Please note that either `resources` and `actions` _or_ `not_resources` and `not_actions` must be defined. From e2ba14b943b9718e2b3f39189ba53a92717e21df Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 12 Jan 2022 12:27:48 +0000 Subject: [PATCH 24/36] Add relay proxy config resource (#168) --- .circleci/config.yml | 3 + CHANGELOG.md | 6 +- launchdarkly/config.go | 2 +- launchdarkly/keys.go | 2 + launchdarkly/policy_statements_helper.go | 20 ++- launchdarkly/provider.go | 23 +-- ..._launchdarkly_relay_proxy_configuration.go | 147 ++++++++++++++++++ ...chdarkly_relay_proxy_configuration_test.go | 140 +++++++++++++++++ website/docs/r/access_token.html.markdown | 4 +- .../r/relay_proxy_configuration.html.markdown | 61 ++++++++ 10 files changed, 390 insertions(+), 18 deletions(-) create mode 100644 launchdarkly/resource_launchdarkly_relay_proxy_configuration.go create mode 100644 launchdarkly/resource_launchdarkly_relay_proxy_configuration_test.go create mode 100644 website/docs/r/relay_proxy_configuration.html.markdown diff --git a/.circleci/config.yml b/.circleci/config.yml index 30294a84..42543851 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,6 +44,9 @@ jobs: - run: name: Test Project Resource command: TESTARGS="-run TestAccProject" make testacc + - run: + name: Test Relay Proxy Configuration Resource + command: TESTARGS="-run TestAccRelayProxy" make testacc - run: name: Test Segment Resource command: TESTARGS="-run TestAccSegment" make testacc diff --git a/CHANGELOG.md b/CHANGELOG.md index 895cbef2..789d94d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ FEATURES: -- Added a new `launchdarkly_audit_log_subscription` resource and data source. +- Added the `launchdarkly_relay_proxy_configuration` for managing configurations for the Relay Proxy's [automatic configuration](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration#writing-an-inline-policy) feature. - ENHANCEMENTS: +- Added a the `launchdarkly_audit_log_subscription` resource and data source for managing LaunchDarkly adit log integration subscriptions. + +ENHANCEMENTS: - Updated tests to use the constant attribute keys defined in launchdarkly/keys.go diff --git a/launchdarkly/config.go b/launchdarkly/config.go index a6b364dc..8b6ac883 100644 --- a/launchdarkly/config.go +++ b/launchdarkly/config.go @@ -14,7 +14,7 @@ import ( ldapi "github.com/launchdarkly/api-client-go/v7" ) -// The version string gets updated at build time using -ldflags +//nolint:staticcheck // The version string gets updated at build time using -ldflags var version = "unreleased" const ( diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 8a151f2c..4bb074ef 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -29,6 +29,7 @@ const ( DEFAULT_TRACK_EVENTS = "default_track_events" DEFAULT_TTL = "default_ttl" DESCRIPTION = "description" + DISPLAY_KEY = "display_key" EFFECT = "effect" EMAIL = "email" ENABLED = "enabled" @@ -40,6 +41,7 @@ const ( FIRST_NAME = "first_name" FLAG_ID = "flag_id" FLAG_KEY = "flag_key" + FULL_KEY = "full_key" ID = "id" INCLUDED = "included" INCLUDE_IN_SNIPPET = "include_in_snippet" diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 1aa24f99..ba0ddb90 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -21,12 +21,12 @@ type policyStatementSchemaOptions struct { func policyStatementsSchema(options policyStatementSchemaOptions) *schema.Schema { schema := &schema.Schema{ Type: schema.TypeList, + Optional: !options.required, + Required: options.required, MinItems: 1, Description: options.description, Deprecated: options.deprecated, ConflictsWith: options.conflictsWith, - Optional: !options.required, - Required: options.required, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ RESOURCES: { @@ -175,3 +175,19 @@ func statementsToStatementReps(policies []ldapi.Statement) []ldapi.StatementRep } return statements } + +// The relay proxy config api requires a statementRep in the POST body +func statementPostsToStatementReps(policies []ldapi.StatementPost) []ldapi.StatementRep { + statements := make([]ldapi.StatementRep, 0, len(policies)) + for _, p := range policies { + rep := ldapi.StatementRep{ + Resources: &p.Resources, + Actions: &p.Actions, + NotResources: p.NotResources, + NotActions: p.NotActions, + Effect: p.Effect, + } + statements = append(statements, rep) + } + return statements +} diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 4ac0f3c0..209f09b5 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -46,17 +46,18 @@ func Provider() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "launchdarkly_project": resourceProject(), - "launchdarkly_environment": resourceEnvironment(), - "launchdarkly_feature_flag": resourceFeatureFlag(), - "launchdarkly_webhook": resourceWebhook(), - "launchdarkly_custom_role": resourceCustomRole(), - "launchdarkly_segment": resourceSegment(), - "launchdarkly_team_member": resourceTeamMember(), - "launchdarkly_feature_flag_environment": resourceFeatureFlagEnvironment(), - "launchdarkly_destination": resourceDestination(), - "launchdarkly_access_token": resourceAccessToken(), - "launchdarkly_audit_log_subscription": resourceAuditLogSubscription(), + "launchdarkly_project": resourceProject(), + "launchdarkly_environment": resourceEnvironment(), + "launchdarkly_feature_flag": resourceFeatureFlag(), + "launchdarkly_webhook": resourceWebhook(), + "launchdarkly_custom_role": resourceCustomRole(), + "launchdarkly_segment": resourceSegment(), + "launchdarkly_team_member": resourceTeamMember(), + "launchdarkly_feature_flag_environment": resourceFeatureFlagEnvironment(), + "launchdarkly_destination": resourceDestination(), + "launchdarkly_access_token": resourceAccessToken(), + "launchdarkly_relay_proxy_configuration": resourceRelayProxyConfig(), + "launchdarkly_audit_log_subscription": resourceAuditLogSubscription(), }, DataSourcesMap: map[string]*schema.Resource{ "launchdarkly_team_member": dataSourceTeamMember(), diff --git a/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go b/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go new file mode 100644 index 00000000..7e4a7aed --- /dev/null +++ b/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go @@ -0,0 +1,147 @@ +package launchdarkly + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + ldapi "github.com/launchdarkly/api-client-go/v7" +) + +func resourceRelayProxyConfig() *schema.Resource { + return &schema.Resource{ + CreateContext: relayProxyConfigCreate, + ReadContext: relayProxyConfigRead, + UpdateContext: relayProxyConfigUpdate, + DeleteContext: relayProxyConfigDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + NAME: { + Type: schema.TypeString, + Required: true, + Description: "A human-friendly name for the Relay Proxy configuration", + }, + POLICY: policyStatementsSchema(policyStatementSchemaOptions{required: true}), + FULL_KEY: { + Type: schema.TypeString, + Sensitive: true, + Computed: true, + Description: "The unique key assigned to the Relay Proxy configuration during creation.", + }, + DISPLAY_KEY: { + Type: schema.TypeString, + Computed: true, + Description: "The last four characters of the full_key.", + }, + }, + } +} + +func relayProxyConfigCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*Client) + + name := d.Get(NAME).(string) + policy, err := policyStatementsFromResourceData(d.Get(POLICY).([]interface{})) + if err != nil { + return diag.FromErr(err) + } + post := ldapi.RelayAutoConfigPost{ + Name: name, + Policy: statementPostsToStatementReps(policy), + } + + proxyConfig, _, err := client.ld.RelayProxyConfigurationsApi.PostRelayAutoConfig(client.ctx).RelayAutoConfigPost(post).Execute() + if err != nil { + return diag.Errorf("failed to create Relay Proxy configuration with name %q: %s", name, handleLdapiErr(err)) + } + + d.SetId(proxyConfig.Id) + + // We only have the valid FULL_KEY immediately after creation. + err = d.Set(FULL_KEY, proxyConfig.FullKey) + if err != nil { + return diag.FromErr(err) + } + + return relayProxyConfigRead(ctx, d, m) +} + +func relayProxyConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := m.(*Client) + + id := d.Id() + proxyConfig, res, err := client.ld.RelayProxyConfigurationsApi.GetRelayProxyConfig(client.ctx, id).Execute() + if isStatusNotFound(res) { + log.Printf("[DEBUG] Relay Proxy configuration with id %q not found on LaunchDarkly. Removing from state", id) + d.SetId("") + return diags + } + if err != nil { + return diag.Errorf("failed to get Relay Proxy configuration with id %q", id) + } + d.SetId(proxyConfig.Id) + + err = d.Set(NAME, proxyConfig.Name) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(POLICY, policyStatementsToResourceData(proxyConfig.Policy)) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(DISPLAY_KEY, proxyConfig.DisplayKey) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func relayProxyConfigUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := m.(*Client) + + id := d.Id() + name := d.Get(NAME).(string) + policy, err := policyStatementsFromResourceData(d.Get(POLICY).([]interface{})) + if err != nil { + return diag.FromErr(err) + } + + patch := []ldapi.PatchOperation{ + patchReplace("/name", &name), + patchReplace("/policy", &policy), + } + + patchWithComment := ldapi.PatchWithComment{ + Patch: patch, + Comment: ldapi.PtrString("Terraform"), + } + + _, _, err = client.ld.RelayProxyConfigurationsApi.PatchRelayAutoConfig(client.ctx, id).PatchWithComment(patchWithComment).Execute() + if err != nil { + return diag.Errorf("failed to update relay proxy configuration with id: %q: %s", id, handleLdapiErr(err)) + } + + return diags +} + +func relayProxyConfigDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := m.(*Client) + + id := d.Id() + _, err := client.ld.RelayProxyConfigurationsApi.DeleteRelayAutoConfig(client.ctx, id).Execute() + if err != nil { + return diag.Errorf("failed to delete relay proxy configuration with id: %q: %s", id, handleLdapiErr(err)) + } + + return diags +} diff --git a/launchdarkly/resource_launchdarkly_relay_proxy_configuration_test.go b/launchdarkly/resource_launchdarkly_relay_proxy_configuration_test.go new file mode 100644 index 00000000..1bf52b97 --- /dev/null +++ b/launchdarkly/resource_launchdarkly_relay_proxy_configuration_test.go @@ -0,0 +1,140 @@ +package launchdarkly + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + testAccRelayProxyConfigCreate = ` +resource "launchdarkly_relay_proxy_configuration" "test" { + name = "example-config" + policy { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/*"] + } +} +` + + testAccRelayProxyConfigUpdate = ` +resource "launchdarkly_relay_proxy_configuration" "test" { + name = "updated-config" + policy { + not_actions = ["*"] + effect = "deny" + not_resources = ["proj/*:env/test"] + } +} +` +) + +func getRelayProxyConfigImportStep(resourceName string) resource.TestStep { + return resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // Because the FULL_KEY is only revealed when the config is created we will never be able to import it + ImportStateVerifyIgnore: []string{FULL_KEY}, + } +} + +func TestAccRelayProxyConfig_Create(t *testing.T) { + resourceName := "launchdarkly_relay_proxy_configuration.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccRelayProxyConfigCreate, + Check: resource.ComposeTestCheckFunc( + testAccCheckRelayProxyConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "example-config"), + resource.TestCheckResourceAttrSet(resourceName, "full_key"), + resource.TestCheckResourceAttrSet(resourceName, "display_key"), + resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.0", "proj/*:env/*"), + ), + }, + getRelayProxyConfigImportStep(resourceName), + }, + }, + ) +} + +func TestAccRelayProxyConfig_Update(t *testing.T) { + resourceName := "launchdarkly_relay_proxy_configuration.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccRelayProxyConfigCreate, + Check: resource.ComposeTestCheckFunc( + testAccCheckRelayProxyConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "example-config"), + resource.TestCheckResourceAttrSet(resourceName, "full_key"), + resource.TestCheckResourceAttrSet(resourceName, "display_key"), + resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.0", "proj/*:env/*"), + ), + }, + getRelayProxyConfigImportStep(resourceName), + { + Config: testAccRelayProxyConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckRelayProxyConfigExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "updated-config"), + resource.TestCheckResourceAttrSet(resourceName, "full_key"), + resource.TestCheckResourceAttrSet(resourceName, "display_key"), + resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "0"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "deny"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.0", "proj/*:env/test"), + ), + }, + getRelayProxyConfigImportStep(resourceName), + }, + }, + ) +} + +func testAccCheckRelayProxyConfigExists(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("webhook ID is not set") + } + + client := testAccProvider.Meta().(*Client) + _, _, err := client.ld.RelayProxyConfigurationsApi.GetRelayProxyConfig(client.ctx, rs.Primary.ID).Execute() + if err != nil { + return fmt.Errorf("received an error getting relay proxy config: %w", err) + } + + return nil + } +} diff --git a/website/docs/r/access_token.html.markdown b/website/docs/r/access_token.html.markdown index 821b0840..ce8bcaff 100644 --- a/website/docs/r/access_token.html.markdown +++ b/website/docs/r/access_token.html.markdown @@ -5,7 +5,7 @@ description: |- Create and manage LaunchDarkly access tokens. --- -# launchdarkly_access +# launchdarkly_access_token Provides a LaunchDarkly access token resource. @@ -15,7 +15,7 @@ This resource allows you to create and manage access tokens within your LaunchDa ## Example Usage -Resource must contain either a `role`, `custom_role` or an `inline_roles` (previously `policy_statements`) block. As of v1.7.0, `policy_statements` has been deprecated in favor of `inline_roles`. +The resource must contain either a `role`, `custom_role` or an `inline_roles` (previously `policy_statements`) block. As of v1.7.0, `policy_statements` has been deprecated in favor of `inline_roles`. With a built-in role diff --git a/website/docs/r/relay_proxy_configuration.html.markdown b/website/docs/r/relay_proxy_configuration.html.markdown new file mode 100644 index 00000000..bed92ac5 --- /dev/null +++ b/website/docs/r/relay_proxy_configuration.html.markdown @@ -0,0 +1,61 @@ +--- +title: "launchdarkly_relay_proxy_configuration" +description: "Create and manage Relay Proxy configurations" +--- + +# launchdarkly_relay_proxy_configuration + +Provides a LaunchDarkly Relay Proxy configuration resource for use with the Relay Proxy's [automatic configuration feature](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration). + +-> **Note:** Relay Proxy automatic configuration is available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). + +This resource allows you to create and manage Relay Proxy configurations within your LaunchDarkly organization. + +-> **Note:** This resource will store the full plaintext secret for your Relay Proxy configuration's unique key in Terraform state. Be sure your state is configured securely before using this resource. See https://www.terraform.io/docs/state/sensitive-data.html for more details. + +## Example Usage + +```hcl +resource "launchdarkly_relay_proxy_configuration" "example" { + name = "example-config" + policy { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/*"] + } +} +``` + +## Argument Reference + +- `name` - (Required) The human-readable name for your Relay Proxy configuration. + +- `policy` - (Required) The Relay Proxy configuration's rule policy block. This determines what content the Relay Proxy receives. To learn more, read [Understanding policies](https://docs.launchdarkly.com/home/members/role-policies#understanding-policies). + +Relay proxy configuration `policy` blocks are composed of the following arguments + +- `effect` - (Required) - Either `allow` or `deny`. This argument defines whether the rule policy allows or denies access to the named resources and actions. + +- `resources` - (Optional) - The list of resource specifiers defining the resources to which the rule policy applies. Either `resources` or `not_resources` must be specified. For a list of available resources read [Understanding resource types and scopes](https://docs.launchdarkly.com/home/account-security/custom-roles/resources#understanding-resource-types-and-scopes). + +- `not_resources` - (Optional) - The list of resource specifiers defining the resources to which the rule policy does not apply. Either `resources` or `not_resources` must be specified. For a list of available resources read [Understanding resource types and scopes](https://docs.launchdarkly.com/home/account-security/custom-roles/resources#understanding-resource-types-and-scopes). + +- `actions` - (Optional) The list of action specifiers defining the actions to which the rule policy applies. Either `actions` or `not_actions` must be specified. For a list of available actions read [Actions reference](https://docs.launchdarkly.com/home/account-security/custom-roles/actions#actions-reference). + +- `not_actions` - (Optional) The list of action specifiers defining the actions to which the rule policy does not apply. Either `actions` or `not_actions` must be specified. For a list of available actions read [Actions reference](https://docs.launchdarkly.com/home/account-security/custom-roles/actions#actions-reference). + +## Attribute Reference + +- `id` - The Relay Proxy configuration's ID + +- `full_key` - The Relay Proxy configuration's unique key. Because the `full_key` is only exposed upon creation, it will not be available if the resource is imported. + +- `display_key` - The last 4 characters of the Relay Proxy configuration's unique key. + +## Import + +Relay Proxy configurations can be imported using the configuration's unique 24 character ID, e.g. + +```shell-session +$ terraform import launchdarkly_relay_proxy_configuration.example 51d440e30c9ff61457c710f6 +``` From 7175e48dd536ace28eef0267928a1535b3e3e8e2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 12 Jan 2022 13:19:27 +0000 Subject: [PATCH 25/36] [sc 137274] add team members data source (#177) * chore: refactor getTeamMember to actually query instead of list * refactor: change teamMember schema to function for reusability and add optional ID * feat: add new data source and related keys * test: add tests for new datasource * docs: add documentation and update changelog * chore: change query logic to filter members in code * Apply suggestions from code review Co-authored-by: Isabelle Miller Co-authored-by: Isabelle Miller --- CHANGELOG.md | 6 +- .../data_source_launchdarkly_team_member.go | 63 ++++---- .../data_source_launchdarkly_team_members.go | 142 ++++++++++++++++++ ...a_source_launchdarkly_team_members_test.go | 121 +++++++++++++++ launchdarkly/keys.go | 3 + launchdarkly/provider.go | 1 + website/docs/d/team_members.html.markdown | 41 +++++ website/launchdarkly.erb | 3 + 8 files changed, 352 insertions(+), 28 deletions(-) create mode 100644 launchdarkly/data_source_launchdarkly_team_members.go create mode 100644 launchdarkly/data_source_launchdarkly_team_members_test.go create mode 100644 website/docs/d/team_members.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 789d94d2..b21e1d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## [2.4.0] (Unreleased) -FEATURES: +FEATURES: + +- Added a `launchdarkly_team_members` data source to allow using multiple team members in one data source. + +ENHANCEMENTS: - Added the `launchdarkly_relay_proxy_configuration` for managing configurations for the Relay Proxy's [automatic configuration](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration#writing-an-inline-policy) feature. diff --git a/launchdarkly/data_source_launchdarkly_team_member.go b/launchdarkly/data_source_launchdarkly_team_member.go index 2aeef7a9..2701bc71 100644 --- a/launchdarkly/data_source_launchdarkly_team_member.go +++ b/launchdarkly/data_source_launchdarkly_team_member.go @@ -9,34 +9,42 @@ import ( ldapi "github.com/launchdarkly/api-client-go/v7" ) +func memberSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + EMAIL: { + Type: schema.TypeString, + Required: true, + }, + ID: { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + FIRST_NAME: { + Type: schema.TypeString, + Computed: true, + }, + LAST_NAME: { + Type: schema.TypeString, + Computed: true, + }, + ROLE: { + Type: schema.TypeString, + Computed: true, + }, + CUSTOM_ROLES: { + Type: schema.TypeSet, + Set: schema.HashString, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + } +} + func dataSourceTeamMember() *schema.Resource { return &schema.Resource{ ReadContext: dataSourceTeamMemberRead, - - Schema: map[string]*schema.Schema{ - EMAIL: { - Type: schema.TypeString, - Required: true, - }, - FIRST_NAME: { - Type: schema.TypeString, - Computed: true, - }, - LAST_NAME: { - Type: schema.TypeString, - Computed: true, - }, - ROLE: { - Type: schema.TypeString, - Computed: true, - }, - CUSTOM_ROLES: { - Type: schema.TypeSet, - Set: schema.HashString, - Elem: &schema.Schema{Type: schema.TypeString}, - Computed: true, - }, - }, + Schema: memberSchema(), } } @@ -44,7 +52,8 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er // this should be the max limit allowed when the member-list-max-limit flag is on teamMemberLimit := int64(1000) - members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Execute() + // After changing this to query by member email, we shouldn't need the limit and recursion on requests, but leaving it in just to be extra safe + members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Filter(fmt.Sprintf("query:%s", memberEmail)).Execute() if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) @@ -56,7 +65,7 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er membersPulled := len(memberItems) for membersPulled < totalMemberCount { offset := int64(membersPulled) - newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Execute() + newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Filter(fmt.Sprintf("query:%s", memberEmail)).Execute() if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) diff --git a/launchdarkly/data_source_launchdarkly_team_members.go b/launchdarkly/data_source_launchdarkly_team_members.go new file mode 100644 index 00000000..8600e76f --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_team_members.go @@ -0,0 +1,142 @@ +package launchdarkly + +import ( + "context" + "crypto/sha1" + "encoding/base64" + "fmt" + "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/v7" +) + +func dataSourceTeamMembers() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceTeamMembersRead, + Schema: map[string]*schema.Schema{ + EMAILS: { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + IGNORE_MISSING: { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + TEAM_MEMBERS: { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: memberSchema(), + }, + }, + }, + } +} + +func dataSourceTeamMembersRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := meta.(*Client) + var members []ldapi.Member + expectedCount := 0 + ignoreMissing := d.Get(IGNORE_MISSING).(bool) + + // Get our members + // There are tradeoffs to be had here + // We've decided to get all the members and filter in code for now, in order to not scale the amount of requests with team_member list size + if emails, ok := d.Get(EMAILS).([]interface{}); ok && len(emails) > 0 { + expectedCount = len(emails) + allMembers, err := getAllTeamMembers(client) + if err != nil { + return diag.FromErr(err) + } + for _, memberEmail := range emails { + var member ldapi.Member + memberFound := false + for _, foundMember := range allMembers { + if foundMember.Email == memberEmail { + member = foundMember + memberFound = true + break + } + } + if !memberFound { + if ignoreMissing { + continue + } + return diag.Errorf("No team member found for email: %s", memberEmail) + } + members = append(members, member) + } + } + + if !ignoreMissing && len(members) != expectedCount { + return diag.Errorf("unexpected number of users returned (%d != %d)", len(members), expectedCount) + } + + // Build our member list + ids := make([]string, 0, len(members)) + memberList := make([]map[string]interface{}, 0, len(members)) + for _, m := range members { + member := make(map[string]interface{}) + member[ID] = m.Id + member[EMAIL] = m.Email + member[FIRST_NAME] = m.FirstName + member[LAST_NAME] = m.LastName + member[ROLE] = m.Role + member[CUSTOM_ROLES] = m.CustomRoles + memberList = append(memberList, member) + ids = append(ids, m.Id) + } + + // Build an ID out of a hash of all the team members ids + h := sha1.New() + if _, err := h.Write([]byte(strings.Join(ids, "-"))); err != nil { + return diag.Errorf("unable to compute hash for IDs: %v", err) + } + d.SetId("team_members#" + base64.URLEncoding.EncodeToString(h.Sum(nil))) + + err := d.Set(TEAM_MEMBERS, memberList) + + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func getAllTeamMembers(client *Client) ([]ldapi.Member, error) { + // this should be the max limit allowed when the member-list-max-limit flag is on + teamMemberLimit := int64(1000) + + // After changing this to query by member email, we shouldn't need the limit and recursion on requests, but leaving it in just to be extra safe + members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Execute() + + if err != nil { + return nil, fmt.Errorf("failed to read team members: %v", handleLdapiErr(err)) + } + + totalMemberCount := int(*members.TotalCount) + + memberItems := members.Items + membersPulled := len(memberItems) + for membersPulled < totalMemberCount { + offset := int64(membersPulled) + newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Execute() + + if err != nil { + return nil, fmt.Errorf("failed to read team members: %v", handleLdapiErr(err)) + } + + memberItems = append(memberItems, newMembers.Items...) + membersPulled = len(memberItems) + } + + return memberItems, nil + +} diff --git a/launchdarkly/data_source_launchdarkly_team_members_test.go b/launchdarkly/data_source_launchdarkly_team_members_test.go new file mode 100644 index 00000000..835b233f --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_team_members_test.go @@ -0,0 +1,121 @@ +package launchdarkly + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + ldapi "github.com/launchdarkly/api-client-go/v7" + "github.com/stretchr/testify/require" +) + +func testAccDataSourceTeamMembersConfig(emails string) string { + return fmt.Sprintf(` +data "launchdarkly_team_members" "test" { + emails = %s + ignore_missing = false +} +`, emails) +} + +func testAccDataSourceTeamMembersConfigIgnoreMissing(emails string) string { + return fmt.Sprintf(` +data "launchdarkly_team_members" "test" { + emails = %s + ignore_missing = true +} +`, emails) +} + +func TestAccDataSourceTeamMembers_noMatchReturnsError(t *testing.T) { + emails := `["does-not-exist@example.com"]` + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceTeamMembersConfig(emails), + ExpectError: regexp.MustCompile(`Error: No team member found for email: does-not-exist@example.com`), + }, + }, + }) +} + +func TestAccDataSourceTeamMembers_noMatchReturnsNoErrorIfIgnoreMissing(t *testing.T) { + emails := `["does-not-exist@example.com"]` + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceTeamMembersConfigIgnoreMissing(emails), + }, + }, + }) +} + +func TestAccDataSourceTeamMembers_exists(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + + // Populate account with dummy team members to ensure pagination is working + teamMemberCount := 15 + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + + teamMembers := make([]ldapi.Member, 0, teamMemberCount) + for i := 0; i < teamMemberCount; i++ { + randomEmail := fmt.Sprintf("%s@example.com", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + member, err := testAccDataSourceTeamMemberCreate(client, randomEmail) + require.NoError(t, err) + teamMembers = append(teamMembers, *member) + } + + resourceName := "data.launchdarkly_team_members.test" + testMember := teamMembers[teamMemberCount-1] + testMember2 := teamMembers[teamMemberCount-2] + testMember3 := teamMembers[teamMemberCount-3] + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceTeamMembersConfig(fmt.Sprintf(`["%s","%s","%s"]`, testMember.Email, testMember2.Email, testMember3.Email)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, IGNORE_MISSING), + resource.TestCheckResourceAttr(resourceName, "team_members.#", "3"), + resource.TestCheckResourceAttr(resourceName, "team_members.0.email", testMember.Email), + resource.TestCheckResourceAttr(resourceName, "team_members.0.first_name", *testMember.FirstName), + resource.TestCheckResourceAttr(resourceName, "team_members.0.last_name", *testMember.LastName), + resource.TestCheckResourceAttr(resourceName, "team_members.0.id", testMember.Id), + resource.TestCheckResourceAttr(resourceName, "team_members.0.role", testMember.Role), + resource.TestCheckResourceAttr(resourceName, "team_members.1.email", testMember2.Email), + resource.TestCheckResourceAttr(resourceName, "team_members.1.first_name", *testMember2.FirstName), + resource.TestCheckResourceAttr(resourceName, "team_members.1.last_name", *testMember2.LastName), + resource.TestCheckResourceAttr(resourceName, "team_members.1.id", testMember2.Id), + resource.TestCheckResourceAttr(resourceName, "team_members.1.role", testMember2.Role), + resource.TestCheckResourceAttr(resourceName, "team_members.2.email", testMember3.Email), + resource.TestCheckResourceAttr(resourceName, "team_members.2.first_name", *testMember3.FirstName), + resource.TestCheckResourceAttr(resourceName, "team_members.2.last_name", *testMember3.LastName), + resource.TestCheckResourceAttr(resourceName, "team_members.2.id", testMember3.Id), + resource.TestCheckResourceAttr(resourceName, "team_members.2.role", testMember3.Role), + ), + }, + }, + }) + for _, member := range teamMembers { + err := testAccDataSourceTeamMemberDelete(client, member.Id) + require.NoError(t, err) + } +} diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index 4bb074ef..dce40423 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -32,6 +32,7 @@ const ( DISPLAY_KEY = "display_key" EFFECT = "effect" EMAIL = "email" + EMAILS = "emails" ENABLED = "enabled" ENVIRONMENTS = "environments" ENV_KEY = "env_key" @@ -43,6 +44,7 @@ const ( FLAG_KEY = "flag_key" FULL_KEY = "full_key" ID = "id" + IGNORE_MISSING = "ignore_missing" INCLUDED = "included" INCLUDE_IN_SNIPPET = "include_in_snippet" INLINE_ROLES = "inline_roles" @@ -78,6 +80,7 @@ const ( STATEMENTS = "statements" TAGS = "tags" TARGETS = "targets" + TEAM_MEMBERS = "team_members" TEMPORARY = "temporary" TOKEN = "token" TRACK_EVENTS = "track_events" diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 209f09b5..a15c8a23 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -61,6 +61,7 @@ func Provider() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ "launchdarkly_team_member": dataSourceTeamMember(), + "launchdarkly_team_members": dataSourceTeamMembers(), "launchdarkly_project": dataSourceProject(), "launchdarkly_environment": dataSourceEnvironment(), "launchdarkly_feature_flag": dataSourceFeatureFlag(), diff --git a/website/docs/d/team_members.html.markdown b/website/docs/d/team_members.html.markdown new file mode 100644 index 00000000..e346218a --- /dev/null +++ b/website/docs/d/team_members.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "launchdarkly" +page_title: "LaunchDarkly: launchdarkly_team_members" +description: |- + Get information about multiple LaunchDarkly team members. +--- + +# launchdarkly_team_members + +Provides a LaunchDarkly team members data source. + +This data source allows you to retrieve team member information from your LaunchDarkly organization on multiple team members. + +## Example Usage + +```hcl +data "launchdarkly_team_member" "example" { + emails = ["example@example.com", "example2@example.com", "example3@example.com"] +} +``` + +## Argument Reference + +- `emails` - (Required) An array of unique email addresses associated with the team members. + +- `ignore_missing` - (Optional) A boolean to determine whether to ignore members that weren't found. + +## Attributes Reference + +In addition to the arguments above, the resource exports the found members as `team_members`. +The following attributes are available for each member: + +- `id` - The 24 character alphanumeric ID of the team member. + +- `first_name` - The team member's given name. + +- `last_name` - The team member's family name. + +- `role` - The role associated with team member. Possible roles are `owner`, `reader`, `writer`, or `admin`. + +- `custom_role` - (Optional) The list of custom roles keys associated with the team member. Custom roles are only available to customers on enterprise plans. To learn more about enterprise plans, contact sales@launchdarkly.com. diff --git a/website/launchdarkly.erb b/website/launchdarkly.erb index 5065c875..9e416b62 100644 --- a/website/launchdarkly.erb +++ b/website/launchdarkly.erb @@ -32,6 +32,9 @@
  • launchdarkly_team_member
  • +
  • + launchdarkly_team_members +
  • From 76f05f287c07459e7a40ae09faf21ae12c7e065b Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Wed, 12 Jan 2022 14:37:05 +0100 Subject: [PATCH 26/36] add flag triggers resource and data source (#163) * Start updating go client * Continue updating resources * feature flag resource * feature flag resource test * approval settings and custom role policies * destination resource * environment * feature flag environment * project * segment * team member * webhook * rule, segment rule, segments, and team member helpers * variations helper * data source tests * webhooks helper * resource tests * variations helper test * policy helpers and tests probably will all fail * custom role and access token probably also broken * policy statements helper update * instantiate config prior to setting other values on it * fix 401 * vendor v7 * update all imports to v7 * missed one * fix nil pointer dereference * instantiate with var instead of make to fix non-empty slice bu * custom roles now complains if you try to set more than one of Role, InlineRole, or CustomRoleIds * fix webhook statemetns * webhook read check if statements are nil & webhook create only set secret if defined * update changelog * fix webhook data source tests * new pointers in client FeatureFlagConfig breaking ff env data source tests * fix negative expiry thing (apparently the api no longer handles it so we have to) * move reset into helper function * hackily handle token reset requests until client bug is fixed * fix variation transformation - can no longer point to empty string * fix notResource and notActions * fix policy helper tests * fix the weird empty string value in variations issue * strip protocol from host if provided * go mod tidy && vendor * update all go packages * clean up commentsg * stub resource functions * stub schema * define instructions * theoretical create function * stub update * stub helper * update schema * refactor schema for data source * not sure if this is the best way to do IDs or not yet * trying compound IDs * set proj env flag keys in read * theoretically handle enabled * ehhhh useless helper function * add test, update instructionsFromResourceData * feat: add enabled update call in create trigger lifecycle * test works with fixed client * set url prior to update * get update working * iron out some bugs * update some descriptions * add flag exist chec to tests * data source test * fix if not found error * failing test * handle IDs for data source * clean up comments * Resource doc * finalize docs * update changelog * typo in changelog * use consts for test attributes * make trigger_url sensiive * update go client * update circle config * mae enabled required * add notes about enterprise feature and sensitive trigger url data to docs * fix IDs * add examples * make context aware * forgot to add website links Co-authored-by: Henry Barrow Co-authored-by: Fabian Feldberg --- .circleci/config.yml | 3 + CHANGELOG.md | 10 +- examples/v2/flag_trigger/example.tf | 39 +++++ .../data_source_launchdarkly_flag_trigger.go | 25 +++ ...a_source_launchdarkly_flag_trigger_test.go | 115 +++++++++++++ launchdarkly/flag_trigger_helper.go | 148 ++++++++++++++++ launchdarkly/keys.go | 2 + launchdarkly/provider.go | 4 +- .../resource_launchdarkly_flag_trigger.go | 160 ++++++++++++++++++ ...resource_launchdarkly_flag_trigger_test.go | 151 +++++++++++++++++ website/docs/d/flag_trigger.html.markdown | 56 ++++++ website/docs/r/flag_trigger.html.markdown | 69 ++++++++ website/launchdarkly.erb | 12 ++ 13 files changed, 791 insertions(+), 3 deletions(-) create mode 100644 examples/v2/flag_trigger/example.tf create mode 100644 launchdarkly/data_source_launchdarkly_flag_trigger.go create mode 100644 launchdarkly/data_source_launchdarkly_flag_trigger_test.go create mode 100644 launchdarkly/flag_trigger_helper.go create mode 100644 launchdarkly/resource_launchdarkly_flag_trigger.go create mode 100644 launchdarkly/resource_launchdarkly_flag_trigger_test.go create mode 100644 website/docs/d/flag_trigger.html.markdown create mode 100644 website/docs/r/flag_trigger.html.markdown diff --git a/.circleci/config.yml b/.circleci/config.yml index 42543851..ee59d2a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,6 +41,9 @@ jobs: - run: name: Test Feature Flag Environment Resource command: TESTARGS="-run TestAccFeatureFlagEnvironment" make testacc + - run: + name: Test Flag Trigger Resource + command: TESTARGS="-run TestAccFlagTrigger" make testacc - run: name: Test Project Resource command: TESTARGS="-run TestAccProject" make testacc diff --git a/CHANGELOG.md b/CHANGELOG.md index b21e1d5b..388ca0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,15 @@ FEATURES: ENHANCEMENTS: +- Added a new `launchdarkly_flag_triggers` resource and data source for managing LaunchDarkly flag triggers. + - Added the `launchdarkly_relay_proxy_configuration` for managing configurations for the Relay Proxy's [automatic configuration](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration#writing-an-inline-policy) feature. -- Added a the `launchdarkly_audit_log_subscription` resource and data source for managing LaunchDarkly adit log integration subscriptions. +- Added a the `launchdarkly_audit_log_subscription` resource and data source for managing LaunchDarkly audit log integration subscriptions. ENHANCEMENTS: -- Updated tests to use the constant attribute keys defined in launchdarkly/keys.go +- Updated tests to use the constant attribute keys defined in launchdarkly/keys.go. - Added a pre-commit file with a hook to alphabetize launchdarkly/keys.go @@ -49,6 +51,10 @@ NOTES: - The `launchdarkly_feature_flag` resource's argument `include_in_snippet` has been deprecated in favor of `client_side_availability`. Please update your config to use `client_side_availability` in order to maintain compatibility with future versions. +ENHANCEMENTS: + +- Upgraded the LaunchDarkly API client to version 7. + ## [2.1.1] (October 11, 2021) BUG FIXES: diff --git a/examples/v2/flag_trigger/example.tf b/examples/v2/flag_trigger/example.tf new file mode 100644 index 00000000..f106ba6e --- /dev/null +++ b/examples/v2/flag_trigger/example.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + launchdarkly = { + source = "launchdarkly/launchdarkly" + version = "~> 2.0" + } + } + required_version = ">= 0.13" +} + +resource "launchdarkly_project" "trigger_test" { + key = "trigger-test" + name = "A Trigger Test Project" + # configure a production environment + environments { + name = "Terraform Production Environment" + key = "production" + color = "581845" + } +} + +resource "launchdarkly_feature_flag" "trigger_test_flag" { + project_key = launchdarkly_project.trigger_test.key + key = "trigger-test-flag" + name = "Trigger Test Flag" + + variation_type = "boolean" +} + +resource "launchdarkly_flag_trigger" "test_trigger" { + project_key = launchdarkly_project.trigger_test.key + env_key = launchdarkly_project.trigger_test.environments.0.key + flag_key = launchdarkly_feature_flag.trigger_test_flag.key + integration_key = "generic-trigger" + instructions { + kind = "turnFlagOff" + } + enabled = false +} diff --git a/launchdarkly/data_source_launchdarkly_flag_trigger.go b/launchdarkly/data_source_launchdarkly_flag_trigger.go new file mode 100644 index 00000000..052e82a7 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_flag_trigger.go @@ -0,0 +1,25 @@ +package launchdarkly + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceFlagTrigger() *schema.Resource { + schemaMap := baseFlagTriggerSchema(true) + schemaMap[ID] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The flag trigger resource ID. This can be found on your trigger URL - please see docs for more info", + } + return &schema.Resource{ + ReadContext: dataSourceFlagTriggerRead, + Schema: schemaMap, + } +} + +func dataSourceFlagTriggerRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return flagTriggerRead(ctx, d, metaRaw, true) +} diff --git a/launchdarkly/data_source_launchdarkly_flag_trigger_test.go b/launchdarkly/data_source_launchdarkly_flag_trigger_test.go new file mode 100644 index 00000000..a16e1d54 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_flag_trigger_test.go @@ -0,0 +1,115 @@ +package launchdarkly + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + ldapi "github.com/launchdarkly/api-client-go/v7" + "github.com/stretchr/testify/require" +) + +const ( + testAccDataSourceFlagTrigger = ` +data "launchdarkly_flag_trigger" "test" { + project_key = "%s" + env_key = "production" + flag_key = "%s" + id = "%s" +} +` +) + +func testAccDataSourceFlagTriggerScaffold(client *Client, projectKey, flagKey string, triggerBody *ldapi.TriggerPost) (*ldapi.TriggerWorkflowRep, error) { + _, err := testAccDataSourceFeatureFlagScaffold(client, projectKey, *ldapi.NewFeatureFlagBody("Trigger Test", flagKey)) + if err != nil { + return nil, err + } + trigger, _, err := client.ld.FlagTriggersApi.CreateTriggerWorkflow(client.ctx, projectKey, "production", flagKey).TriggerPost(*triggerBody).Execute() + if err != nil { + return nil, err + } + return &trigger, nil +} + +func TestAccDataSourceFlagTrigger_noMatchReturnsError(t *testing.T) { + id := "nonexistent-id" + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + flagKey := "trigger-test" + _, err = testAccDataSourceFeatureFlagScaffold(client, projectKey, *ldapi.NewFeatureFlagBody("Trigger Test", flagKey)) + require.NoError(t, err) + + defer func() { + err := testAccDataSourceProjectDelete(client, projectKey) + require.NoError(t, err) + }() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceFlagTrigger, projectKey, flagKey, id), + // the integration key will not appear here since it is not set on the data source + ExpectError: regexp.MustCompile(`Error: failed to get trigger with ID `), + }, + }, + }) +} + +func TestAccDataSourceFlagTrigger_exists(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + flagKey := "trigger-test" + instructions := []map[string]interface{}{{"kind": "turnFlagOff"}} + post := ldapi.NewTriggerPost("datadog") + post.Instructions = &instructions + trigger, err := testAccDataSourceFlagTriggerScaffold(client, projectKey, flagKey, post) + require.NoError(t, err) + + defer func() { + err := testAccDataSourceProjectDelete(client, projectKey) + require.NoError(t, err) + }() + + resourceName := "data.launchdarkly_flag_trigger.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceFlagTrigger, projectKey, flagKey, *trigger.Id), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", *trigger.Id), + resource.TestCheckResourceAttrSet(resourceName, "maintainer_id"), + resource.TestCheckResourceAttrSet(resourceName, "enabled"), + resource.TestCheckResourceAttr(resourceName, "instructions.0.kind", "turnFlagOff"), + resource.TestCheckResourceAttr(resourceName, "project_key", projectKey), + resource.TestCheckResourceAttr(resourceName, "env_key", "production"), + resource.TestCheckResourceAttr(resourceName, "flag_key", flagKey), + resource.TestCheckResourceAttr(resourceName, "integration_key", *trigger.IntegrationKey), + ), + }, + }, + }) + +} diff --git a/launchdarkly/flag_trigger_helper.go b/launchdarkly/flag_trigger_helper.go new file mode 100644 index 00000000..f1dd3ec1 --- /dev/null +++ b/launchdarkly/flag_trigger_helper.go @@ -0,0 +1,148 @@ +package launchdarkly + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func baseFlagTriggerSchema(isDataSource bool) map[string]*schema.Schema { + return map[string]*schema.Schema{ + PROJECT_KEY: { + Type: schema.TypeString, + Required: true, + Description: "The LaunchDarkly project key", + ForceNew: true, + ValidateDiagFunc: validateKey(), + }, + ENV_KEY: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The LaunchDarkly environment key", + }, + FLAG_KEY: { + Type: schema.TypeString, + Required: true, + Description: "The key of the feature flag the trigger acts upon", + ForceNew: true, + ValidateDiagFunc: validateKey(), + }, + INTEGRATION_KEY: { + Type: schema.TypeString, + Required: !isDataSource, + Optional: isDataSource, + Description: "The unique identifier of the integration you intend to set your trigger up with. \"generic-trigger\" should be used for integrations not explicitly supported.", + ForceNew: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"generic-trigger", "datadog", "dynatrace", "honeycomb", "new-relic-apm", "signalfx"}, false)), + }, + INSTRUCTIONS: { + Type: schema.TypeList, + Required: !isDataSource, + Optional: isDataSource, + Description: "Instructions containing the action to perform when triggering. Currently supported flag actions are \"turnFlagOn\" and \"turnFlagOff\".", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + KIND: { + Type: schema.TypeString, + Required: true, + Description: "The action to perform when triggering. Currently supported flag actions are \"turnFlagOn\" and \"turnFlagOff\".", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"turnFlagOn", "turnFlagOff"}, false)), + }, + }, + }, + }, + TRIGGER_URL: { + Type: schema.TypeString, + Computed: true, + Description: "The unique trigger URL", + Sensitive: true, + }, + MAINTAINER_ID: { + Type: schema.TypeString, + Computed: true, + Description: "The LaunchDarkly ID of the member who maintains the trigger. The API will automatically apply the member associated with your Terraform API key or the most recently-set maintainer", + }, + ENABLED: { + Type: schema.TypeBool, + Required: !isDataSource, + Optional: isDataSource, + Description: "Whether the trigger is currently active or not. This property defaults to true upon creation", + }, + } +} + +func flagTriggerRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}, isDataSource bool) diag.Diagnostics { + var diags diag.Diagnostics + client := metaRaw.(*Client) + integrationKey := d.Get(INTEGRATION_KEY).(string) + projectKey := d.Get(PROJECT_KEY).(string) + envKey := d.Get(ENV_KEY).(string) + flagKey := d.Get(FLAG_KEY).(string) + + var triggerId string + if isDataSource { + triggerId = d.Get(ID).(string) + } else { + triggerId = d.Id() + } + + trigger, res, err := client.ld.FlagTriggersApi.GetTriggerWorkflowById(client.ctx, projectKey, flagKey, envKey, triggerId).Execute() + // if the trigger does not exist it simply return an empty trigger object + if (isStatusNotFound(res) || trigger.Id == nil) && !isDataSource { + log.Printf("[WARN] failed to find %s trigger with ID %s, removing from state if present", integrationKey, triggerId) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("[WARN] failed to find %s trigger with ID %s, removing from state if present", integrationKey, triggerId), + }) + d.SetId("") + return diags + } + if err != nil || trigger.Id == nil { + return diag.Errorf("failed to get %s trigger with ID %s", integrationKey, triggerId) + } + + if isDataSource { + d.SetId(*trigger.Id) + } + _ = d.Set(PROJECT_KEY, projectKey) + _ = d.Set(ENV_KEY, envKey) + _ = d.Set(FLAG_KEY, flagKey) + _ = d.Set(INTEGRATION_KEY, *trigger.IntegrationKey) + _ = d.Set(INSTRUCTIONS, *trigger.Instructions) + _ = d.Set(MAINTAINER_ID, trigger.MaintainerId) + _ = d.Set(ENABLED, trigger.Enabled) + // NOTE: we do not want to set the trigger url at any point past the create as it will always be obscured + + return diags +} + +func instructionsFromResourceData(d *schema.ResourceData, method string) []map[string]interface{} { + rawInstructions := d.Get(INSTRUCTIONS).([]interface{}) + var instructions []map[string]interface{} + switch method { + case "POST": + for _, v := range rawInstructions { + instructions = append(instructions, v.(map[string]interface{})) + } + case "PATCH": + if d.HasChange(INSTRUCTIONS) { + for _, v := range rawInstructions { + oldInstruction := v.(map[string]interface{}) + value := oldInstruction[KIND] + instructions = append(instructions, map[string]interface{}{ + KIND: "replaceTriggerActionInstructions", + VALUE: []map[string]interface{}{{ + KIND: value, + }, + }}) + } + } + } + return instructions +} diff --git a/launchdarkly/keys.go b/launchdarkly/keys.go index dce40423..67737024 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -48,6 +48,7 @@ const ( INCLUDED = "included" INCLUDE_IN_SNIPPET = "include_in_snippet" INLINE_ROLES = "inline_roles" + INSTRUCTIONS = "instructions" INTEGRATION_KEY = "integration_key" KEY = "key" KIND = "kind" @@ -84,6 +85,7 @@ const ( TEMPORARY = "temporary" TOKEN = "token" TRACK_EVENTS = "track_events" + TRIGGER_URL = "trigger_url" URL = "url" USING_ENVIRONMENT_ID = "using_environment_id" USING_MOBILE_KEY = "using_mobile_key" diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index a15c8a23..6077dfed 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -56,8 +56,9 @@ func Provider() *schema.Provider { "launchdarkly_feature_flag_environment": resourceFeatureFlagEnvironment(), "launchdarkly_destination": resourceDestination(), "launchdarkly_access_token": resourceAccessToken(), - "launchdarkly_relay_proxy_configuration": resourceRelayProxyConfig(), + "launchdarkly_flag_trigger": resourceFlagTrigger(), "launchdarkly_audit_log_subscription": resourceAuditLogSubscription(), + "launchdarkly_relay_proxy_configuration": resourceRelayProxyConfig(), }, DataSourcesMap: map[string]*schema.Resource{ "launchdarkly_team_member": dataSourceTeamMember(), @@ -68,6 +69,7 @@ func Provider() *schema.Provider { "launchdarkly_feature_flag_environment": dataSourceFeatureFlagEnvironment(), "launchdarkly_webhook": dataSourceWebhook(), "launchdarkly_segment": dataSourceSegment(), + "launchdarkly_flag_trigger": dataSourceFlagTrigger(), "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), }, ConfigureFunc: providerConfigure, diff --git a/launchdarkly/resource_launchdarkly_flag_trigger.go b/launchdarkly/resource_launchdarkly_flag_trigger.go new file mode 100644 index 00000000..7ba39e71 --- /dev/null +++ b/launchdarkly/resource_launchdarkly_flag_trigger.go @@ -0,0 +1,160 @@ +package launchdarkly + +import ( + "context" + "fmt" + "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/v7" +) + +func resourceFlagTrigger() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceFlagTriggerCreate, + ReadContext: resourceFlagTriggerRead, + UpdateContext: resourceFlagTriggerUpdate, + DeleteContext: resourceFlagTriggerDelete, + Exists: resourceFlagTriggerExists, + + Importer: &schema.ResourceImporter{ + StateContext: resourceFlagTriggerImport, + }, + Schema: baseFlagTriggerSchema(false), + } +} + +func resourceFlagTriggerCreate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + envKey := d.Get(ENV_KEY).(string) + flagKey := d.Get(FLAG_KEY).(string) + integrationKey := d.Get(INTEGRATION_KEY).(string) + instructions := instructionsFromResourceData(d, "POST") + + enabled := d.Get(ENABLED).(bool) + + triggerBody := ldapi.NewTriggerPost(integrationKey) + triggerBody.Instructions = &instructions + + preUpdateTrigger, _, err := client.ld.FlagTriggersApi.CreateTriggerWorkflow(client.ctx, projectKey, envKey, flagKey).TriggerPost(*triggerBody).Execute() + if err != nil { + return diag.Errorf("failed to create %s trigger for proj/env/flag %s/%s/%s: %s", integrationKey, projectKey, envKey, flagKey, err.Error()) + } + _ = d.Set(TRIGGER_URL, preUpdateTrigger.TriggerURL) + + // if enabled is false upon creation, we need to do a patch since the create endpoint + // does not accept multiple instructions + var postUpdateTrigger ldapi.TriggerWorkflowRep + if !enabled { + instructions = []map[string]interface{}{{ + KIND: "disableTrigger", + }} + input := ldapi.FlagTriggerInput{ + Instructions: &instructions, + } + + postUpdateTrigger, _, err = client.ld.FlagTriggersApi.PatchTriggerWorkflow(client.ctx, projectKey, envKey, flagKey, *preUpdateTrigger.Id).FlagTriggerInput(input).Execute() + if err != nil { + return diag.Errorf("failed to update %s trigger for proj/env/flag %s/%s/%s: %s", integrationKey, projectKey, envKey, flagKey, err.Error()) + } + } + + d.SetId(*postUpdateTrigger.Id) + return resourceFlagTriggerRead(ctx, d, metaRaw) +} + +func resourceFlagTriggerRead(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + return flagTriggerRead(ctx, d, metaRaw, false) +} + +func resourceFlagTriggerUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + projectKey := d.Get(PROJECT_KEY).(string) + envKey := d.Get(ENV_KEY).(string) + flagKey := d.Get(FLAG_KEY).(string) + integrationKey := d.Get(INTEGRATION_KEY).(string) + instructions := instructionsFromResourceData(d, "PATCH") + + triggerId := d.Id() + + oldEnabled, newEnabled := d.GetChange(ENABLED) + if oldEnabled.(bool) != newEnabled.(bool) { + if newEnabled.(bool) { + instructions = append(instructions, map[string]interface{}{ + KIND: "enableTrigger", + }) + } else { + instructions = append(instructions, map[string]interface{}{ + KIND: "disableTrigger", + }) + } + } + input := ldapi.FlagTriggerInput{ + Instructions: &instructions, + } + + _, _, err := client.ld.FlagTriggersApi.PatchTriggerWorkflow(client.ctx, projectKey, envKey, flagKey, triggerId).FlagTriggerInput(input).Execute() + if err != nil { + return diag.Errorf("failed to update %s trigger for proj/env/flag %s/%s/%s", integrationKey, projectKey, envKey, flagKey) + } + return resourceFlagTriggerRead(ctx, d, metaRaw) +} + +func resourceFlagTriggerDelete(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { + client := metaRaw.(*Client) + integrationKey := d.Get(INTEGRATION_KEY).(string) + projectKey := d.Get(PROJECT_KEY).(string) + envKey := d.Get(ENV_KEY).(string) + flagKey := d.Get(FLAG_KEY).(string) + + triggerId := d.Id() + + _, err := client.ld.FlagTriggersApi.DeleteTriggerWorkflow(client.ctx, projectKey, envKey, flagKey, triggerId).Execute() + if err != nil { + return diag.Errorf("failed to delete %s trigger with ID %s for proj/env/flag %s/%s/%s", integrationKey, triggerId, projectKey, envKey, flagKey) + } + return diag.Diagnostics{} +} + +func resourceFlagTriggerExists(d *schema.ResourceData, metaRaw interface{}) (bool, error) { + client := metaRaw.(*Client) + integrationKey := d.Get(INTEGRATION_KEY).(string) + projectKey := d.Get(PROJECT_KEY).(string) + envKey := d.Get(ENV_KEY).(string) + flagKey := d.Get(FLAG_KEY).(string) + + triggerId := d.Id() + + _, res, err := client.ld.FlagTriggersApi.GetTriggerWorkflowById(client.ctx, projectKey, flagKey, envKey, triggerId).Execute() + if isStatusNotFound(res) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to check if %s trigger with ID %s exists in proj/env/flag %s/%s/%s: %s", integrationKey, triggerId, projectKey, envKey, flagKey, handleLdapiErr(err)) + } + return true, nil +} + +func resourceFlagTriggerImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectKey, envKey, flagKey, triggerId, err := triggerImportIdToKeys(d.Id()) + if err != nil { + return nil, err + } + d.SetId(triggerId) + + _ = d.Set(PROJECT_KEY, projectKey) + _ = d.Set(ENV_KEY, envKey) + _ = d.Set(FLAG_KEY, flagKey) + return []*schema.ResourceData{d}, nil +} + +func triggerImportIdToKeys(id string) (projectKey string, envKey string, flagKey string, triggerId string, err error) { + if strings.Count(id, "/") != 3 { + return "", "", "", "", fmt.Errorf("found unexpected trigger id format: %q expected format: 'project_key/env_key/flag_key/trigger_id'", triggerId) + } + parts := strings.SplitN(id, "/", 4) + projectKey, envKey, flagKey, triggerId = parts[0], parts[1], parts[2], parts[3] + return projectKey, envKey, flagKey, triggerId, nil +} diff --git a/launchdarkly/resource_launchdarkly_flag_trigger_test.go b/launchdarkly/resource_launchdarkly_flag_trigger_test.go new file mode 100644 index 00000000..ec61389a --- /dev/null +++ b/launchdarkly/resource_launchdarkly_flag_trigger_test.go @@ -0,0 +1,151 @@ +package launchdarkly + +import ( + "fmt" + "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 ( + testAccFlagTriggerCreate = ` +resource "launchdarkly_flag_trigger" "basic" { + project_key = launchdarkly_project.test.key + env_key = "test" + flag_key = launchdarkly_feature_flag.trigger_flag.key + integration_key = "generic-trigger" + instructions { + kind = "turnFlagOn" + } + enabled = false +} +` + testAccFlagTriggerUpdate = ` +resource "launchdarkly_flag_trigger" "basic" { + project_key = launchdarkly_project.test.key + env_key = "test" + flag_key = launchdarkly_feature_flag.trigger_flag.key + integration_key = "generic-trigger" + instructions { + kind = "turnFlagOff" + } + enabled = true +} +` + + testAccFlagTriggerUpdate2 = ` +resource "launchdarkly_flag_trigger" "basic" { + project_key = launchdarkly_project.test.key + env_key = "test" + flag_key = launchdarkly_feature_flag.trigger_flag.key + integration_key = "generic-trigger" + instructions { + kind = "turnFlagOff" + } + enabled = false +} +` +) + +func withRandomFlag(randomFlag, resource string) string { + return fmt.Sprintf(` + resource "launchdarkly_feature_flag" "trigger_flag" { + project_key = launchdarkly_project.test.key + key = "%s" + name = "Basic feature flag" + variation_type = "boolean" + } + + %s`, randomFlag, resource) +} + +func TestAccFlagTrigger_CreateUpdate(t *testing.T) { + projectKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + flagKey := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_flag_trigger.basic" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: withRandomProject(projectKey, withRandomFlag(flagKey, testAccFlagTriggerCreate)), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFlagExists(projectKey, "launchdarkly_feature_flag.trigger_flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, FLAG_KEY, flagKey), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, "generic-trigger"), + resource.TestCheckResourceAttr(resourceName, "instructions.0.kind", "turnFlagOn"), + resource.TestCheckResourceAttr(resourceName, ENABLED, "false"), + resource.TestCheckResourceAttrSet(resourceName, TRIGGER_URL), + resource.TestCheckResourceAttrSet(resourceName, MAINTAINER_ID), + ), + }, + { + Config: withRandomProject(projectKey, withRandomFlag(flagKey, testAccFlagTriggerUpdate)), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFlagExists(projectKey, "launchdarkly_feature_flag.trigger_flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, FLAG_KEY, flagKey), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, "generic-trigger"), + resource.TestCheckResourceAttr(resourceName, "instructions.0.kind", "turnFlagOff"), + resource.TestCheckResourceAttr(resourceName, ENABLED, "true"), + resource.TestCheckResourceAttrSet(resourceName, TRIGGER_URL), + resource.TestCheckResourceAttrSet(resourceName, MAINTAINER_ID), + ), + }, + { + Config: withRandomProject(projectKey, withRandomFlag(flagKey, testAccFlagTriggerUpdate2)), + Check: resource.ComposeTestCheckFunc( + testAccCheckProjectExists("launchdarkly_project.test"), + testAccCheckFlagExists(projectKey, "launchdarkly_feature_flag.trigger_flag"), + resource.TestCheckResourceAttr(resourceName, PROJECT_KEY, projectKey), + resource.TestCheckResourceAttr(resourceName, ENV_KEY, "test"), + resource.TestCheckResourceAttr(resourceName, FLAG_KEY, flagKey), + resource.TestCheckResourceAttr(resourceName, INTEGRATION_KEY, "generic-trigger"), + resource.TestCheckResourceAttr(resourceName, "instructions.0.kind", "turnFlagOff"), + resource.TestCheckResourceAttr(resourceName, ENABLED, "false"), + resource.TestCheckResourceAttrSet(resourceName, TRIGGER_URL), + resource.TestCheckResourceAttrSet(resourceName, MAINTAINER_ID), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdPrefix: fmt.Sprintf("%s/test/%s/", projectKey, flagKey), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{TRIGGER_URL}, + }, + }, + }) +} + +func testAccCheckFlagExists(projectKey, 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("flag ID is not set") + } + projectKey, flagKey, err := flagIdToKeys(rs.Primary.ID) + if err != nil { + return fmt.Errorf("flag ID is not set correctly") + } + + client := testAccProvider.Meta().(*Client) + _, _, err = client.ld.FeatureFlagsApi.GetFeatureFlag(client.ctx, projectKey, flagKey).Execute() + if err != nil { + return fmt.Errorf("received an error getting flag. %s", err) + } + return nil + } +} diff --git a/website/docs/d/flag_trigger.html.markdown b/website/docs/d/flag_trigger.html.markdown new file mode 100644 index 00000000..4c5ad5ab --- /dev/null +++ b/website/docs/d/flag_trigger.html.markdown @@ -0,0 +1,56 @@ +--- +layout: "launchdarkly" +page_title: "LaunchDarkly: launchdarkly_flag_trigger" +description: |- + Get information about LaunchDarkly flag trigers. +--- + +# launchdarkly_flag_trigger + +Provides a LaunchDarkly flag trigger data source. + +-> **Note:** Flag triggers are available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). + +This data source allows you to retrieve information about flag triggers from your LaunchDarkly organization. + +## Example Usage + +```hcl +data "launchdarkly_flag_trigger" "example" { + id = "///61d490757f7821150815518f" + integration_key = "datadog" + instructions { + kind = "turnFlagOff" + } +} +``` + +## Argument Reference + +- `id` - (Required) The Terraform trigger ID. This ID takes the following format: `///`. The unique trigger ID can be found in your saved trigger URL: + +``` +https://app.launchdarkly.com/webhook/triggers//aff25a53-17d9-4112-a9b8-12718d1a2e79 +``` + +Please note that if you did not save this upon creation of the resource, you will have to reset it to get a new value, which can cause breaking changes. + +## Attributes Reference + +In addition to the arguments above, the resource exports the following attributes: + +- `project_key` - The unique key of the project encompassing the associated flag. + +- `env_key` - The unique key of the environment the flag trigger will work in. + +- `flag_key` - The unique key of the associated flag. + +- `integration_key` - The unique identifier of the integration your trigger is set up with. + +- `instructions` - Instructions containing the action to perform when invoking the trigger. Currently supported flag actions are `"turnFlagOn"` and `"turnFlagOff"`. These can be found on the `kind` field nested on the `instructions` attribute. + +- `maintainer_id` - The ID of the member responsible for maintaining the flag trigger. If created via Terraform, this value will be the ID of the member associated with the API key used for your provider configuration. + +- `enabled` - Whether the trigger is currently active or not. + +Please note that the original trigger URL itself will not be surfaced. diff --git a/website/docs/r/flag_trigger.html.markdown b/website/docs/r/flag_trigger.html.markdown new file mode 100644 index 00000000..39e44746 --- /dev/null +++ b/website/docs/r/flag_trigger.html.markdown @@ -0,0 +1,69 @@ +--- +layout: "launchdarkly" +page_title: "LaunchDarkly: launchdarkly_flag_trigger" +description: |- + Create and manage LaunchDarkly flag triggers. +--- + +# launchdarkly_project + +Provides a LaunchDarkly flag trigger resource. + +-> **Note:** Flag triggers are available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). + +This resource allows you to create and manage flag triggers within your LaunchDarkly organization. + +-> **Note:** This resource will store sensitive unique trigger URL value in plaintext in your Terraform state. Be sure your state is configured securely before using this resource. See https://www.terraform.io/docs/state/sensitive-data.html for more details. + +## Example Usage + +```hcl +resource "launchdarkly_flag_trigger" "example" { + project_key = launchdarkly_project.example.key + env_key = "test" + flag_key = launchdarkly_feature_flag.trigger_flag.key + integration_key = "generic-trigger" + instructions { + kind = "turnFlagOn" + } + enabled = false +} +``` + +## Argument Reference + +- `project_key` - (Required) The unique key of the project encompassing the associated flag. + +- `env_key` - (Required) The unique key of the environment the flag trigger will work in. + +- `flag_key` - (Required) The unique key of the associated flag. + +- `integration_key` - (Required) The unique identifier of the integration you intend to set your trigger up with. Currently supported are `"datadog"`, `"dynatrace"`, `"honeycomb"`, `"new-relic-apm"`, `"signalfx"`, and `"generic-trigger"`. `"generic-trigger"` should be used for integrations not explicitly supported. + +- `instructions` - (Required) Instructions containing the action to perform when invoking the trigger. Currently supported flag actions are `"turnFlagOn"` and `"turnFlagOff"`. This must be passed as the key-value pair `{ kind = "" }`. + +- `enabled` - (Optional) Whether the trigger is currently active or not. This property defaults to true upon creation and will thereafter conform to the last Terraform-configured value. + +## Additional Attributes + +In addition to the above arguments, this resource supports the following computed attributes: + +`trigger_url` - The unique URL used to invoke the trigger. + +`maintainer_id` - The ID of the member responsible for maintaining the flag trigger. If created via Terraform, this value will be the ID of the member associated with the API key used for your provider configuration. + +## Import + +LaunchDarkly flag triggers can be imported using the following syntax: + +``` +$ terraform import launchdarkly_flag_trigger.example /// +``` + +The unique trigger ID can be found in your saved trigger URL: + +``` +https://app.launchdarkly.com/webhook/triggers//aff25a53-17d9-4112-a9b8-12718d1a2e79 +``` + +Please note that if you did not save this upon creation of the resource, you will have to reset it to get a new value, which can cause breaking changes. diff --git a/website/launchdarkly.erb b/website/launchdarkly.erb index 9e416b62..747e9a8d 100644 --- a/website/launchdarkly.erb +++ b/website/launchdarkly.erb @@ -11,6 +11,9 @@
  • Data Sources
  • From 2ab32b1944853590c2ee24d1b998fd5083104432 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 14 Jan 2022 14:22:37 +0000 Subject: [PATCH 29/36] [sc 137802] emails need to be url escaped when querying (#178) * fix: url escape member emails when querying * test: update tests to ensure we also use an escapable char (+) --- launchdarkly/data_source_launchdarkly_team_member.go | 5 +++-- launchdarkly/data_source_launchdarkly_team_member_test.go | 2 +- launchdarkly/data_source_launchdarkly_team_members_test.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/launchdarkly/data_source_launchdarkly_team_member.go b/launchdarkly/data_source_launchdarkly_team_member.go index 2701bc71..a70d58de 100644 --- a/launchdarkly/data_source_launchdarkly_team_member.go +++ b/launchdarkly/data_source_launchdarkly_team_member.go @@ -3,6 +3,7 @@ package launchdarkly import ( "context" "fmt" + "net/url" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -53,7 +54,7 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er teamMemberLimit := int64(1000) // After changing this to query by member email, we shouldn't need the limit and recursion on requests, but leaving it in just to be extra safe - members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Filter(fmt.Sprintf("query:%s", memberEmail)).Execute() + members, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Filter(fmt.Sprintf("query:%s", url.QueryEscape(memberEmail))).Execute() if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) @@ -65,7 +66,7 @@ func getTeamMemberByEmail(client *Client, memberEmail string) (*ldapi.Member, er membersPulled := len(memberItems) for membersPulled < totalMemberCount { offset := int64(membersPulled) - newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Filter(fmt.Sprintf("query:%s", memberEmail)).Execute() + newMembers, _, err := client.ld.AccountMembersApi.GetMembers(client.ctx).Limit(teamMemberLimit).Offset(offset).Filter(fmt.Sprintf("query:%s", url.QueryEscape(memberEmail))).Execute() if err != nil { return nil, fmt.Errorf("failed to read team member with email: %s: %v", memberEmail, handleLdapiErr(err)) diff --git a/launchdarkly/data_source_launchdarkly_team_member_test.go b/launchdarkly/data_source_launchdarkly_team_member_test.go index 6e653f49..ff74d947 100644 --- a/launchdarkly/data_source_launchdarkly_team_member_test.go +++ b/launchdarkly/data_source_launchdarkly_team_member_test.go @@ -70,7 +70,7 @@ func TestAccDataSourceTeamMember_exists(t *testing.T) { teamMembers := make([]ldapi.Member, 0, teamMemberCount) for i := 0; i < teamMemberCount; i++ { - randomEmail := fmt.Sprintf("%s@example.com", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + randomEmail := fmt.Sprintf("%s@example.com", acctest.RandStringFromCharSet(10, "abcdefghijklmnopqrstuvwxyz012346789+")) member, err := testAccDataSourceTeamMemberCreate(client, randomEmail) require.NoError(t, err) teamMembers = append(teamMembers, *member) diff --git a/launchdarkly/data_source_launchdarkly_team_members_test.go b/launchdarkly/data_source_launchdarkly_team_members_test.go index 835b233f..3e6042f2 100644 --- a/launchdarkly/data_source_launchdarkly_team_members_test.go +++ b/launchdarkly/data_source_launchdarkly_team_members_test.go @@ -74,7 +74,7 @@ func TestAccDataSourceTeamMembers_exists(t *testing.T) { teamMembers := make([]ldapi.Member, 0, teamMemberCount) for i := 0; i < teamMemberCount; i++ { - randomEmail := fmt.Sprintf("%s@example.com", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + randomEmail := fmt.Sprintf("%s@example.com", acctest.RandStringFromCharSet(10, "abcdefghijklmnopqrstuvwxyz012346789+")) member, err := testAccDataSourceTeamMemberCreate(client, randomEmail) require.NoError(t, err) teamMembers = append(teamMembers, *member) From 2f88a685cef7171e161f315b51abf7f9f7356580 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 17 Jan 2022 17:26:03 +0000 Subject: [PATCH 30/36] Ffeldberg/sc 136454/use configurecontextfunc instead of configurefunc (#176) * feat: make providerConfigure context aware * fix: fix typo * WIP: attempt to pass in context to NewClient (breaks everything) * Revert "WIP: attempt to pass in context to NewClient (breaks everything)" This reverts commit 09a082042bb0fe3e09db62544683b1a559b804e9. --- launchdarkly/provider.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 040aab88..402c0e74 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -1,10 +1,11 @@ package launchdarkly import ( - "fmt" + "context" "net/url" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -74,11 +75,12 @@ func Provider() *schema.Provider { "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), "launchdarkly_metric": dataSourceMetric(), }, - ConfigureFunc: providerConfigure, + ConfigureContextFunc: providerConfigure, } } -func providerConfigure(d *schema.ResourceData) (interface{}, error) { +func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { + var diags diag.Diagnostics host := d.Get(api_host).(string) if strings.HasPrefix(host, "http") { u, _ := url.Parse(host) @@ -88,12 +90,20 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { oauthToken := d.Get(oauth_token).(string) if oauthToken == "" && accessToken == "" { - return nil, fmt.Errorf("either an %q or %q must be specified", access_token, oauth_token) + return nil, diag.Errorf("either an %q or %q must be specified", access_token, oauth_token) } if oauthToken != "" { - return newClient(oauthToken, host, true) + client, err := newClient(oauthToken, host, true) + if err != nil { + return client, diag.FromErr(err) + } + return client, diags } - return newClient(accessToken, host, false) + client, err := newClient(accessToken, host, false) + if err != nil { + return client, diag.FromErr(err) + } + return client, diags } From 1b1ea7e9c320b190fc67d6e5d71e9c1c81bc8b62 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Tue, 18 Jan 2022 18:00:38 +0100 Subject: [PATCH 31/36] Imiller/sc 137617/tf doc improvements (#180) * specfy location of relay proxy id * integationKey should be forcenew * specify force new in docs * add missing notes about enterprise plans --- launchdarkly/audit_log_subscription_helper.go | 1 + website/docs/r/access_token.html.markdown | 5 +++-- website/docs/r/audit_log_subscription.html.markdown | 2 +- website/docs/r/custom_role.html.markdown | 6 +++--- website/docs/r/destination.html.markdown | 8 +++++--- website/docs/r/environment.html.markdown | 4 ++-- website/docs/r/feature_flag.html.markdown | 5 ++--- .../docs/r/feature_flag_environment.html.markdown | 4 ++-- website/docs/r/flag_trigger.html.markdown | 8 ++++---- website/docs/r/metric.html.markdown | 12 ++++++------ website/docs/r/project.html.markdown | 5 +++-- .../docs/r/relay_proxy_configuration.html.markdown | 6 ++++++ website/docs/r/segment.html.markdown | 6 +++--- website/docs/r/team_member.html.markdown | 2 +- 14 files changed, 42 insertions(+), 32 deletions(-) diff --git a/launchdarkly/audit_log_subscription_helper.go b/launchdarkly/audit_log_subscription_helper.go index f5dfe3c9..02be4802 100644 --- a/launchdarkly/audit_log_subscription_helper.go +++ b/launchdarkly/audit_log_subscription_helper.go @@ -32,6 +32,7 @@ func auditLogSubscriptionSchema(isDataSource bool) map[string]*schema.Schema { Required: true, // we are omitting appdynamics for now because it requires oauth ValidateFunc: validation.StringNotInSlice([]string{"appdynamics"}, false), + ForceNew: true, }, NAME: { Type: schema.TypeString, diff --git a/website/docs/r/access_token.html.markdown b/website/docs/r/access_token.html.markdown index ce8bcaff..16bf93f6 100644 --- a/website/docs/r/access_token.html.markdown +++ b/website/docs/r/access_token.html.markdown @@ -53,8 +53,9 @@ resource "launchdarkly_access_token" "token_with_policy_statements" { - `name` - (Optional) A human-friendly name for the access token. -- `service_token` - (Optional) Whether the token will be a [service token](https://docs.launchdarkly.com/home/account-security/api-access-tokens#service-tokens) -- `default_api_version` - (Optional) The default API version for this token. Defaults to the latest API version. +- `service_token` - (Optional) Whether the token will be a [service token](https://docs.launchdarkly.com/home/account-security/api-access-tokens#service-tokens). A change in this field will force the destruction of the existing token and the creation of a new one. + +- `default_api_version` - (Optional) The default API version for this token. Defaults to the latest API version. A change in this field will force the destruction of the existing token in state and the creation of a new one. An access token may have its permissions specified by a built-in LaunchDarkly role, a set of custom role keys, or by an inline custom role (policy statements). diff --git a/website/docs/r/audit_log_subscription.html.markdown b/website/docs/r/audit_log_subscription.html.markdown index c0f6928c..9a266673 100644 --- a/website/docs/r/audit_log_subscription.html.markdown +++ b/website/docs/r/audit_log_subscription.html.markdown @@ -35,7 +35,7 @@ resource "launchdarkly_audit_log_subscription" "example" { ## Argument Reference -- `integration_key` (Required) The integration key. As of January 2022, supported integrations are `"datadog"`, `"dynatrace"`, `"elastic"`, `"honeycomb"`, `"logdna"`, `"msteams"`, `"new-relic-apm"`, `"signalfx"`, and `"splunk"`. +- `integration_key` (Required) The integration key. As of January 2022, supported integrations are `"datadog"`, `"dynatrace"`, `"elastic"`, `"honeycomb"`, `"logdna"`, `"msteams"`, `"new-relic-apm"`, `"signalfx"`, and `"splunk"`. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` (Required) - A human-friendly name for your audit log subscription viewable from within the LaunchDarkly Integrations page. diff --git a/website/docs/r/custom_role.html.markdown b/website/docs/r/custom_role.html.markdown index 6b20ca9b..c2c9deea 100644 --- a/website/docs/r/custom_role.html.markdown +++ b/website/docs/r/custom_role.html.markdown @@ -9,9 +9,9 @@ description: |- Provides a LaunchDarkly custom role resource. -This resource allows you to create and manage custom roles within your LaunchDarkly organization. +-> **Note:** Custom roles are available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). --> **Note:** Custom roles are only available to customers on enterprise plans. To learn more about enterprise plans, contact sales@launchdarkly.com. +This resource allows you to create and manage custom roles within your LaunchDarkly organization. ## Example Usage @@ -36,7 +36,7 @@ resource "launchdarkly_custom_role" "example" { ## Argument Reference -- `key` - (Required) The unique key that references the custom role. +- `key` - (Required) The unique key that references the custom role. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The human-readable name for the custom role. diff --git a/website/docs/r/destination.html.markdown b/website/docs/r/destination.html.markdown index 396515a5..5a3ed0a2 100644 --- a/website/docs/r/destination.html.markdown +++ b/website/docs/r/destination.html.markdown @@ -9,6 +9,8 @@ description: |- Provides a LaunchDarkly Data Export Destination resource. +-> **Note:** Data Export is available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). + Data Export Destinations are locations that receive exported data. This resource allows you to configure destinations for the export of raw analytics data, including feature flag requests, analytics events, custom events, and more. To learn more about data export, read [Data Export Documentation](https://docs.launchdarkly.com/integrations/data-export). @@ -98,13 +100,13 @@ resource "launchdarkly_destination" "example" { ## Argument Reference -- `project_key` - (Required) - The LaunchDarkly project key. +- `project_key` - (Required) - The LaunchDarkly project key. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `env_key` - (Required) - The environment key. +- `env_key` - (Required) - The environment key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) - A human-readable name for your data export destination. -- `kind` - (Required) - The data export destination type. Available choices are `kinesis`, `google-pubsub`, `mparticle`, `azure-event-hubs`, and `segment`. +- `kind` - (Required) - The data export destination type. Available choices are `kinesis`, `google-pubsub`, `mparticle`, `azure-event-hubs`, and `segment`. A change in this field will force the destruction of the existing resource and the creation of a new one. - `config` - (Required) - The destination-specific configuration. To learn more, read [Destination-Specific Configs](#destination-specific-configs). diff --git a/website/docs/r/environment.html.markdown b/website/docs/r/environment.html.markdown index 1d6782e6..f6770191 100644 --- a/website/docs/r/environment.html.markdown +++ b/website/docs/r/environment.html.markdown @@ -46,11 +46,11 @@ resource "launchdarkly_environment" "approvals_example" { ## Argument Reference -- `project_key` - (Required) - The environment's project key. +- `project_key` - (Required) - The environment's project key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The name of the environment. -- `key` - (Required) The project-unique key for the environment. +- `key` - (Required) The project-unique key for the environment. A change in this field will force the destruction of the existing resource and the creation of a new one. - `color` - (Required) The color swatch as an RGB hex value with no leading `#`. For example: `000000`. diff --git a/website/docs/r/feature_flag.html.markdown b/website/docs/r/feature_flag.html.markdown index 7e4bdd0f..4568e942 100644 --- a/website/docs/r/feature_flag.html.markdown +++ b/website/docs/r/feature_flag.html.markdown @@ -76,9 +76,9 @@ resource "launchdarkly_feature_flag" "json_example" { ## Argument Reference -- `project_key` - (Required) The feature flag's project key. +- `project_key` - (Required) The feature flag's project key. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `key` - (Required) The unique feature flag key that references the flag in your application code. +- `key` - (Required) The unique feature flag key that references the flag in your application code. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The human-readable name of the feature flag. @@ -102,7 +102,6 @@ resource "launchdarkly_feature_flag" "json_example" { - `custom_properties` - (Optional) List of nested blocks describing the feature flag's [custom properties](https://docs.launchdarkly.com/docs/custom-properties). To learn more, read [Nested Custom Properties](#nested-custom-properties). - ### Nested Variations Blocks Nested `variations` blocks have the following structure: diff --git a/website/docs/r/feature_flag_environment.html.markdown b/website/docs/r/feature_flag_environment.html.markdown index 6a907177..337d678a 100644 --- a/website/docs/r/feature_flag_environment.html.markdown +++ b/website/docs/r/feature_flag_environment.html.markdown @@ -59,9 +59,9 @@ resource "launchdarkly_feature_flag_environment" "number_env" { ## Argument Reference -- `flag_id` - (Required) The feature flag's unique `id` in the format `project_key/flag_key`. +- `flag_id` - (Required) The feature flag's unique `id` in the format `project_key/flag_key`. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `env_key` - (Required) The environment key. +- `env_key` - (Required) The environment key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `on` (previously `targeting_enabled`) - (Optional) Whether targeting is enabled. Defaults to `false` if not set. diff --git a/website/docs/r/flag_trigger.html.markdown b/website/docs/r/flag_trigger.html.markdown index a14b6112..3a235940 100644 --- a/website/docs/r/flag_trigger.html.markdown +++ b/website/docs/r/flag_trigger.html.markdown @@ -32,13 +32,13 @@ resource "launchdarkly_flag_trigger" "example" { ## Argument Reference -- `project_key` - (Required) The unique key of the project encompassing the associated flag. +- `project_key` - (Required) The unique key of the project encompassing the associated flag. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `env_key` - (Required) The unique key of the environment the flag trigger will work in. +- `env_key` - (Required) The unique key of the environment the flag trigger will work in. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `flag_key` - (Required) The unique key of the associated flag. +- `flag_key` - (Required) The unique key of the associated flag. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `integration_key` - (Required) The unique identifier of the integration you intend to set your trigger up with. Currently supported are `"datadog"`, `"dynatrace"`, `"honeycomb"`, `"new-relic-apm"`, `"signalfx"`, and `"generic-trigger"`. `"generic-trigger"` should be used for integrations not explicitly supported. +- `integration_key` - (Required) The unique identifier of the integration you intend to set your trigger up with. Currently supported are `"datadog"`, `"dynatrace"`, `"honeycomb"`, `"new-relic-apm"`, `"signalfx"`, and `"generic-trigger"`. `"generic-trigger"` should be used for integrations not explicitly supported. A change in this field will force the destruction of the existing resource and the creation of a new one. - `instructions` - (Required) Instructions containing the action to perform when invoking the trigger. Currently supported flag actions are `"turnFlagOn"` and `"turnFlagOff"`. This must be passed as the key-value pair `{ kind = "" }`. diff --git a/website/docs/r/metric.html.markdown b/website/docs/r/metric.html.markdown index b69532d8..4b1b8c24 100644 --- a/website/docs/r/metric.html.markdown +++ b/website/docs/r/metric.html.markdown @@ -32,13 +32,13 @@ resource "launchdarkly_metric" "example" { ## Argument Reference -- `key` - (Required) The unique key that references the metric. +- `key` - (Required) The unique key that references the metric. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `project_key` - (Required) The metrics's project key. +- `project_key` - (Required) The metrics's project key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The human-friendly name for the metric. -- `kind` - (Required) The metric type. Available choices are `click`, `custom`, and `pageview`. +- `kind` - (Required) The metric type. Available choices are `click`, `custom`, and `pageview`. A change in this field will force the destruction of the existing resource and the creation of a new one. - `description` - (Optional) The description of the metric's purpose. @@ -50,15 +50,15 @@ resource "launchdarkly_metric" "example" { - `maintainerId` - (Optional) The userId of the user maintaining the metric. -- `selector` - (Required for kind `click`) The CSS selector for `click` metrics. +- `selector` - (Required for kind `click`) The CSS selector for `click` metrics. - `urls` - (Required for kind `click` and `pageview`) A block determining which URLs the metric watches. To learn more, read [Nested Urls Blocks](#nested-urls-blocks). -- `event_key` - (Required for kind `custom`) The event key to watch for `custom` metrics. +- `event_key` - (Required for kind `custom`) The event key to watch for `custom` metrics. - `success_criteria` - (Required for kind `custom`) The success criteria for numeric `custom` metrics. -- `unit` - (Required for kind `custom`) The unit for numeric `custom` metrics. +- `unit` - (Required for kind `custom`) The unit for numeric `custom` metrics. ### Nested Urls Blocks diff --git a/website/docs/r/project.html.markdown b/website/docs/r/project.html.markdown index 829f894d..5bb54310 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -46,11 +46,12 @@ resource "launchdarkly_project" "example" { ## Argument Reference -- `key` - (Required) The project's unique key. +- `key` - (Required) The project's unique key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The project's name. - `environments` - (Required) List of nested `environments` blocks describing LaunchDarkly environments that belong to the project. When managing LaunchDarkly projects in Terraform, you should always manage your environments as nested project resources. To learn more, read [Nested Environments Blocks](#nested-environments-blocks). + ### Nested Environments Blocks -> **Note:** Mixing the use of nested `environments` blocks and [`launchdarkly_environment`](/docs/providers/launchdarkly/r/environment.html) resources is not recommended. `launchdarkly_environment` resources should only be used when the encapsulating project is not managed in Terraform. @@ -67,7 +68,7 @@ Nested `environments` blocks have the following structure: - `name` - (Required) The name of the environment. -- `key` - (Required) The project-unique key for the environment. +- `key` - (Required) The project-unique key for the environment. A change in this field will force the destruction of the existing environment and the creation of a new one. - `color` - (Required) The color swatch as an RGB hex value with no leading `#`. For example: `000000`. diff --git a/website/docs/r/relay_proxy_configuration.html.markdown b/website/docs/r/relay_proxy_configuration.html.markdown index bed92ac5..19cc5253 100644 --- a/website/docs/r/relay_proxy_configuration.html.markdown +++ b/website/docs/r/relay_proxy_configuration.html.markdown @@ -59,3 +59,9 @@ Relay Proxy configurations can be imported using the configuration's unique 24 c ```shell-session $ terraform import launchdarkly_relay_proxy_configuration.example 51d440e30c9ff61457c710f6 ``` + +The unique relay proxy ID can be found in the relay proxy edit page URL, which you can locate by clicking the three dot menu on your relay proxy item in the UI and selecting 'Edit configuration': + +``` +https://app.launchdarkly.com/settings/relay//edit +``` diff --git a/website/docs/r/segment.html.markdown b/website/docs/r/segment.html.markdown index abcd3c35..159ccfab 100644 --- a/website/docs/r/segment.html.markdown +++ b/website/docs/r/segment.html.markdown @@ -37,11 +37,11 @@ resource "launchdarkly_segment" "example" { ## Argument Reference -- `key` - (Required) The unique key that references the segment. +- `key` - (Required) The unique key that references the segment. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `project_key` - (Required) The segment's project key. +- `project_key` - (Required) The segment's project key. A change in this field will force the destruction of the existing resource and the creation of a new one. -- `env_key` - (Required) The segment's environment key. +- `env_key` - (Required) The segment's environment key. A change in this field will force the destruction of the existing resource and the creation of a new one. - `name` - (Required) The human-friendly name for the segment. diff --git a/website/docs/r/team_member.html.markdown b/website/docs/r/team_member.html.markdown index 85c1d571..fceb3575 100644 --- a/website/docs/r/team_member.html.markdown +++ b/website/docs/r/team_member.html.markdown @@ -26,7 +26,7 @@ resource "launchdarkly_team_member" "example" { ## Argument Reference -- `email` - (Required) The unique email address associated with the team member. +- `email` - (Required) The unique email address associated with the team member. A change in this field will force the destruction of the existing resource and the creation of a new one. - `first_name` - (Optional) The team member's given name. Please note that, once created, this cannot be updated except by the team member themself. From 944a7238552b6e311abfc9403c75d5e606d1a357 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 19 Jan 2022 09:36:09 +0000 Subject: [PATCH 32/36] Ffeldberg/sc 137794/escalation terraform provider throws error (#179) * chore: bump go client version * fix: update to account for new go client version * todo: add debugging comment * chore: update policy_statement Schema to use conflictswith * revert schema changes with conflictswith.. not possible https://github.com/hashicorp/terraform-plugin-sdk/issues/71 * fix: add resources/notresources and actions/notactions as required * test: add not_resource and not_actions tests * fix: fix the hashing for pointers in subfields * fix unit tests and update auditlog config through hook --- go.mod | 2 +- go.sum | 4 +- .../audit_log_subscription_configs.json | 2 +- ...aunchdarkly_audit_log_subscription_test.go | 6 +- .../data_source_launchdarkly_webhook_test.go | 6 +- launchdarkly/policies_helper.go | 39 ++++++-- launchdarkly/policy_statements_helper.go | 51 ++++++---- launchdarkly/policy_statements_helper_test.go | 28 +++--- .../resource_launchdarkly_custom_role.go | 10 +- .../resource_launchdarkly_custom_role_test.go | 95 +++++++++++++++++++ 10 files changed, 191 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 06072f1e..c555234f 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.0 github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 // indirect github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect - github.com/launchdarkly/api-client-go/v7 v7.1.0 + github.com/launchdarkly/api-client-go/v7 v7.1.1 github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect diff --git a/go.sum b/go.sum index 99b4d58c..c4f80cd5 100644 --- a/go.sum +++ b/go.sum @@ -273,8 +273,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/launchdarkly/api-client-go/v7 v7.1.0 h1:A9+vYgtaM8fFmOn04qAGYvUH1OhnX8dGEZUWkKlE11g= -github.com/launchdarkly/api-client-go/v7 v7.1.0/go.mod h1:GVl1inKsWoKX3yLgdqrjxWw8k4ih0HlSmdnrhi5NNDs= +github.com/launchdarkly/api-client-go/v7 v7.1.1 h1:3VBkFt9xHljMw5KDlVFDUogxfH78Y7GLVu8irBC8Gy8= +github.com/launchdarkly/api-client-go/v7 v7.1.1/go.mod h1:GVl1inKsWoKX3yLgdqrjxWw8k4ih0HlSmdnrhi5NNDs= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/launchdarkly/audit_log_subscription_configs.json b/launchdarkly/audit_log_subscription_configs.json index 28a0add9..c6ab2b60 100644 --- a/launchdarkly/audit_log_subscription_configs.json +++ b/launchdarkly/audit_log_subscription_configs.json @@ -1 +1 @@ -{"appdynamics": {"account": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "applicationID": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "datadog": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "hostURL": {"type": "enum", "isOptional": true, "allowedValues": ["https://api.datadoghq.com", "https://api.datadoghq.eu"], "defaultValue": "https://api.datadoghq.com", "isSecret": false}}, "dynatrace": {"apiToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "entity": {"type": "enum", "isOptional": true, "allowedValues": ["APPLICATION", "APPLICATION_METHOD", "APPLICATION_METHOD_GROUP", "AUTO_SCALING_GROUP", "AUXILIARY_SYNTHETIC_TEST", "AWS_APPLICATION_LOAD_BALANCER", "AWS_AVAILABILITY_ZONE", "AWS_CREDENTIALS", "AWS_LAMBDA_FUNCTION", "AWS_NETWORK_LOAD_BALANCER", "AZURE_API_MANAGEMENT_SERVICE", "AZURE_APPLICATION_GATEWAY", "AZURE_COSMOS_DB", "AZURE_CREDENTIALS", "AZURE_EVENT_HUB", "AZURE_EVENT_HUB_NAMESPACE", "AZURE_FUNCTION_APP", "AZURE_IOT_HUB", "AZURE_LOAD_BALANCER", "AZURE_MGMT_GROUP", "AZURE_REDIS_CACHE", "AZURE_REGION", "AZURE_SERVICE_BUS_NAMESPACE", "AZURE_SERVICE_BUS_QUEUE", "AZURE_SERVICE_BUS_TOPIC", "AZURE_SQL_DATABASE", "AZURE_SQL_ELASTIC_POOL", "AZURE_SQL_SERVER", "AZURE_STORAGE_ACCOUNT", "AZURE_SUBSCRIPTION", "AZURE_TENANT", "AZURE_VM", "AZURE_VM_SCALE_SET", "AZURE_WEB_APP", "CF_APPLICATION", "CF_FOUNDATION", "CINDER_VOLUME", "CLOUD_APPLICATION", "CLOUD_APPLICATION_INSTANCE", "CLOUD_APPLICATION_NAMESPACE", "CONTAINER_GROUP", "CONTAINER_GROUP_INSTANCE", "CUSTOM_APPLICATION", "CUSTOM_DEVICE", "CUSTOM_DEVICE_GROUP", "DCRUM_APPLICATION", "DCRUM_SERVICE", "DCRUM_SERVICE_INSTANCE", "DEVICE_APPLICATION_METHOD", "DISK", "DOCKER_CONTAINER_GROUP_INSTANCE", "DYNAMO_DB_TABLE", "EBS_VOLUME", "EC2_INSTANCE", "ELASTIC_LOAD_BALANCER", "ENVIRONMENT", "EXTERNAL_SYNTHETIC_TEST_STEP", "GCP_ZONE", "GEOLOCATION", "GEOLOC_SITE", "GOOGLE_COMPUTE_ENGINE", "HOST", "HOST_GROUP", "HTTP_CHECK", "HTTP_CHECK_STEP", "HYPERVISOR", "KUBERNETES_CLUSTER", "KUBERNETES_NODE", "MOBILE_APPLICATION", "NETWORK_INTERFACE", "NEUTRON_SUBNET", "OPENSTACK_PROJECT", "OPENSTACK_REGION", "OPENSTACK_VM", "OS", "PROCESS_GROUP", "PROCESS_GROUP_INSTANCE", "RELATIONAL_DATABASE_SERVICE", "SERVICE", "SERVICE_INSTANCE", "SERVICE_METHOD", "SERVICE_METHOD_GROUP", "SWIFT_CONTAINER", "SYNTHETIC_LOCATION", "SYNTHETIC_TEST", "SYNTHETIC_TEST_STEP", "VIRTUALMACHINE", "VMWARE_DATACENTER"], "defaultValue": "APPLICATION", "isSecret": false}}, "elastic": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "index": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "honeycomb": {"datasetName": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}}, "logdna": {"ingestionKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "level": {"type": "string", "isOptional": true, "allowedValues": null, "defaultValue": "INFO", "isSecret": false}}, "msteams": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "new-relic-apm": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "applicationId": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "domain": {"type": "enum", "isOptional": true, "allowedValues": ["api.newrelic.com", "api.eu.newrelic.com"], "defaultValue": "api.newrelic.com", "isSecret": false}}, "signalfx": {"accessToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "realm": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "splunk": {"base-url": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "skip-ca-verification": {"type": "boolean", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}} \ No newline at end of file +{"appdynamics": {"account": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "applicationID": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "datadog": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "hostURL": {"type": "enum", "isOptional": true, "allowedValues": ["https://api.datadoghq.com", "https://api.datadoghq.eu", "https://us3.datadoghq.com", "https://us5.datadoghq.com", "https://app.ddog-gov.com"], "defaultValue": "https://api.datadoghq.com", "isSecret": false}}, "dynatrace": {"apiToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "entity": {"type": "enum", "isOptional": true, "allowedValues": ["APPLICATION", "APPLICATION_METHOD", "APPLICATION_METHOD_GROUP", "AUTO_SCALING_GROUP", "AUXILIARY_SYNTHETIC_TEST", "AWS_APPLICATION_LOAD_BALANCER", "AWS_AVAILABILITY_ZONE", "AWS_CREDENTIALS", "AWS_LAMBDA_FUNCTION", "AWS_NETWORK_LOAD_BALANCER", "AZURE_API_MANAGEMENT_SERVICE", "AZURE_APPLICATION_GATEWAY", "AZURE_COSMOS_DB", "AZURE_CREDENTIALS", "AZURE_EVENT_HUB", "AZURE_EVENT_HUB_NAMESPACE", "AZURE_FUNCTION_APP", "AZURE_IOT_HUB", "AZURE_LOAD_BALANCER", "AZURE_MGMT_GROUP", "AZURE_REDIS_CACHE", "AZURE_REGION", "AZURE_SERVICE_BUS_NAMESPACE", "AZURE_SERVICE_BUS_QUEUE", "AZURE_SERVICE_BUS_TOPIC", "AZURE_SQL_DATABASE", "AZURE_SQL_ELASTIC_POOL", "AZURE_SQL_SERVER", "AZURE_STORAGE_ACCOUNT", "AZURE_SUBSCRIPTION", "AZURE_TENANT", "AZURE_VM", "AZURE_VM_SCALE_SET", "AZURE_WEB_APP", "CF_APPLICATION", "CF_FOUNDATION", "CINDER_VOLUME", "CLOUD_APPLICATION", "CLOUD_APPLICATION_INSTANCE", "CLOUD_APPLICATION_NAMESPACE", "CONTAINER_GROUP", "CONTAINER_GROUP_INSTANCE", "CUSTOM_APPLICATION", "CUSTOM_DEVICE", "CUSTOM_DEVICE_GROUP", "DCRUM_APPLICATION", "DCRUM_SERVICE", "DCRUM_SERVICE_INSTANCE", "DEVICE_APPLICATION_METHOD", "DISK", "DOCKER_CONTAINER_GROUP_INSTANCE", "DYNAMO_DB_TABLE", "EBS_VOLUME", "EC2_INSTANCE", "ELASTIC_LOAD_BALANCER", "ENVIRONMENT", "EXTERNAL_SYNTHETIC_TEST_STEP", "GCP_ZONE", "GEOLOCATION", "GEOLOC_SITE", "GOOGLE_COMPUTE_ENGINE", "HOST", "HOST_GROUP", "HTTP_CHECK", "HTTP_CHECK_STEP", "HYPERVISOR", "KUBERNETES_CLUSTER", "KUBERNETES_NODE", "MOBILE_APPLICATION", "NETWORK_INTERFACE", "NEUTRON_SUBNET", "OPENSTACK_PROJECT", "OPENSTACK_REGION", "OPENSTACK_VM", "OS", "PROCESS_GROUP", "PROCESS_GROUP_INSTANCE", "RELATIONAL_DATABASE_SERVICE", "SERVICE", "SERVICE_INSTANCE", "SERVICE_METHOD", "SERVICE_METHOD_GROUP", "SWIFT_CONTAINER", "SYNTHETIC_LOCATION", "SYNTHETIC_TEST", "SYNTHETIC_TEST_STEP", "VIRTUALMACHINE", "VMWARE_DATACENTER"], "defaultValue": "APPLICATION", "isSecret": false}}, "elastic": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "index": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "honeycomb": {"datasetName": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}}, "logdna": {"ingestionKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "level": {"type": "string", "isOptional": true, "allowedValues": null, "defaultValue": "INFO", "isSecret": false}}, "msteams": {"url": {"type": "uri", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "new-relic-apm": {"apiKey": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "applicationId": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "domain": {"type": "enum", "isOptional": true, "allowedValues": ["api.newrelic.com", "api.eu.newrelic.com"], "defaultValue": "api.newrelic.com", "isSecret": false}}, "signalfx": {"accessToken": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "realm": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}, "splunk": {"base-url": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}, "token": {"type": "string", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": true}, "skip-ca-verification": {"type": "boolean", "isOptional": false, "allowedValues": null, "defaultValue": null, "isSecret": false}}} \ No newline at end of file diff --git a/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go b/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go index 5337d5d7..01dc454a 100644 --- a/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go +++ b/launchdarkly/data_source_launchdarkly_audit_log_subscription_test.go @@ -28,10 +28,12 @@ data "launchdarkly_audit_log_subscription" "test" { ) func testAccDataSourceAuditLogSubscriptionCreate(client *Client, integrationKey string, subscriptionBody ldapi.SubscriptionPost) (*ldapi.Integration, error) { + statementResources := []string{"proj/*"} + statementActions := []string{"*"} statements := []ldapi.StatementPost{{ Effect: "allow", - Resources: []string{"proj/*"}, - Actions: []string{"*"}, + Resources: &statementResources, + Actions: &statementActions, }} subscriptionBody.Statements = &statements diff --git a/launchdarkly/data_source_launchdarkly_webhook_test.go b/launchdarkly/data_source_launchdarkly_webhook_test.go index 4255e41e..af7e0b80 100644 --- a/launchdarkly/data_source_launchdarkly_webhook_test.go +++ b/launchdarkly/data_source_launchdarkly_webhook_test.go @@ -21,6 +21,8 @@ data "launchdarkly_webhook" "test" { ) func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi.Webhook, error) { + statementResources := []string{"proj/*"} + statementActions := []string{"turnFlagOn"} webhookBody := ldapi.WebhookPost{ Url: "https://www.example.com", Sign: false, @@ -29,8 +31,8 @@ func testAccDataSourceWebhookCreate(client *Client, webhookName string) (*ldapi. Tags: &[]string{"terraform"}, Statements: &[]ldapi.StatementPost{ { - Resources: []string{"proj/*"}, - Actions: []string{"turnFlagOn"}, + Resources: &statementResources, + Actions: &statementActions, Effect: "allow", }, }, diff --git a/launchdarkly/policies_helper.go b/launchdarkly/policies_helper.go index 5105427e..1e4ae8c3 100644 --- a/launchdarkly/policies_helper.go +++ b/launchdarkly/policies_helper.go @@ -53,20 +53,24 @@ func policiesFromResourceData(d *schema.ResourceData) []ldapi.StatementPost { func policyFromResourceData(val interface{}) ldapi.StatementPost { policyMap := val.(map[string]interface{}) - p := ldapi.StatementPost{ - Resources: []string{}, - Actions: []string{}, - Effect: policyMap[EFFECT].(string), - } + statementResources := []string{} + statementActions := []string{} + for _, r := range policyMap[RESOURCES].([]interface{}) { - p.Resources = append(p.Resources, r.(string)) + statementResources = append(statementResources, r.(string)) } for _, a := range policyMap[ACTIONS].([]interface{}) { - p.Actions = append(p.Actions, a.(string)) + statementActions = append(statementActions, a.(string)) } - sort.Strings(p.Actions) - sort.Strings(p.Resources) + sort.Strings(statementActions) + sort.Strings(statementResources) + + p := ldapi.StatementPost{ + Resources: &statementResources, + Actions: &statementActions, + Effect: policyMap[EFFECT].(string), + } return p } @@ -83,8 +87,23 @@ func policiesToResourceData(policies []ldapi.Statement) interface{} { return transformed } +// https://godoc.org/github.com/hashicorp/terraform/helper/schema#SchemaSetFunc +type hashStatement struct { + Resources []string + Actions []string + Effect string +} + // https://godoc.org/github.com/hashicorp/terraform/helper/schema#SchemaSetFunc func policyHash(val interface{}) int { - policy := policyFromResourceData(val) + rawPolicy := policyFromResourceData(val) + // since this function runs once for each sub-field (unclear why) + // it was creating 3 different hash indices per policy since it was hashing the + // pointer addresses rather than the values themselves + policy := hashStatement{ + Resources: *rawPolicy.Resources, + Actions: *rawPolicy.Actions, + Effect: rawPolicy.Effect, + } return schema.HashString(fmt.Sprintf("%v", policy)) } diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index ba0ddb90..8eeca87a 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -2,6 +2,7 @@ package launchdarkly import ( "errors" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -104,36 +105,56 @@ func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.S if err != nil { return statements, err } - s := policyStatementFromResourceData(statement) + s, err := policyStatementFromResourceData(statement) + if err != nil { + return statements, err + } statements = append(statements, s) } return statements, nil } -func policyStatementFromResourceData(statement map[string]interface{}) ldapi.StatementPost { +func policyStatementFromResourceData(statement map[string]interface{}) (ldapi.StatementPost, error) { + statementResources := []string{} + statementActions := []string{} + statementNotResources := []string{} + statementNotActions := []string{} ret := ldapi.StatementPost{ Effect: statement[EFFECT].(string), } + // Build our policy fields for _, r := range statement[RESOURCES].([]interface{}) { - ret.Resources = append(ret.Resources, r.(string)) + statementResources = append(statementResources, r.(string)) } for _, a := range statement[ACTIONS].([]interface{}) { - ret.Actions = append(ret.Actions, a.(string)) + statementActions = append(statementActions, a.(string)) } // optional fields rawNotResources := statement[NOT_RESOURCES].([]interface{}) - var notResources []string for _, n := range rawNotResources { - notResources = append(notResources, n.(string)) - ret.NotResources = ¬Resources + statementNotResources = append(statementNotResources, n.(string)) } rawNotActions := statement[NOT_ACTIONS].([]interface{}) - var notActions []string for _, n := range rawNotActions { - notActions = append(notActions, n.(string)) - ret.NotActions = ¬Actions + statementNotActions = append(statementNotActions, n.(string)) + } + // Add the appropriate fields to the statement + if len(statement[RESOURCES].([]interface{})) > 0 { + ret.Resources = &statementResources + } else if len(statement[NOT_RESOURCES].([]interface{})) > 0 { + ret.NotResources = &statementNotResources + } else { + return ret, fmt.Errorf("please provide either 'resources' or not_resources' for your policy_statement") } - return ret + if len(statement[ACTIONS].([]interface{})) > 0 { + ret.Actions = &statementActions + } else if len(statement[NOT_ACTIONS].([]interface{})) > 0 { + ret.NotActions = &statementNotActions + } else { + return ret, fmt.Errorf("please provide either 'actions' or not_actions' for your policy_statement") + } + + return ret, nil } func policyStatementsToResourceData(statements []ldapi.StatementRep) []interface{} { @@ -180,13 +201,7 @@ func statementsToStatementReps(policies []ldapi.Statement) []ldapi.StatementRep func statementPostsToStatementReps(policies []ldapi.StatementPost) []ldapi.StatementRep { statements := make([]ldapi.StatementRep, 0, len(policies)) for _, p := range policies { - rep := ldapi.StatementRep{ - Resources: &p.Resources, - Actions: &p.Actions, - NotResources: p.NotResources, - NotActions: p.NotActions, - Effect: p.Effect, - } + rep := ldapi.StatementRep(p) statements = append(statements, rep) } return statements diff --git a/launchdarkly/policy_statements_helper_test.go b/launchdarkly/policy_statements_helper_test.go index 2efddfe1..30e7ca26 100644 --- a/launchdarkly/policy_statements_helper_test.go +++ b/launchdarkly/policy_statements_helper_test.go @@ -10,6 +10,12 @@ import ( ) func TestPolicyStatementsRoundTripConversion(t *testing.T) { + statementResources := []string{"proj/*"} + statementActions := []string{"*"} + statementPostResources1 := []string{"proj/*:env/*;qa_*"} + statementPostResources2 := []string{"proj/*:env/*;qa_*:/flag/*"} + statementPostActions := []string{"*"} + testCases := []struct { name string policyStatements map[string]interface{} @@ -28,8 +34,8 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, expected: []ldapi.StatementPost{ { - Resources: []string{"proj/*"}, - Actions: []string{"*"}, + Resources: &statementResources, + Actions: &statementActions, Effect: "allow", }, }, @@ -52,13 +58,13 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, expected: []ldapi.StatementPost{ { - Resources: []string{"proj/*:env/*;qa_*"}, - Actions: []string{"*"}, + Resources: &statementPostResources1, + Actions: &statementPostActions, Effect: "allow", }, { - Resources: []string{"proj/*:env/*;qa_*:/flag/*"}, - Actions: []string{"*"}, + Resources: &statementPostResources2, + Actions: &statementPostActions, Effect: "allow", }, }, @@ -77,7 +83,7 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { expected: []ldapi.StatementPost{ { NotResources: strArrayPtr([]string{"proj/*:env/production:flag/*"}), - Actions: []string{"*"}, + Actions: &statementPostActions, Effect: "allow", }, }, @@ -170,13 +176,7 @@ func statementPostsToStatements(posts []ldapi.StatementPost) []ldapi.Statement { var statements []ldapi.Statement for _, p := range posts { p := p - statement := ldapi.Statement{ - Resources: &p.Resources, - NotResources: p.NotResources, - Actions: &p.Actions, - NotActions: p.NotActions, - Effect: p.Effect, - } + statement := ldapi.Statement(p) statements = append(statements, statement) } return statements diff --git a/launchdarkly/resource_launchdarkly_custom_role.go b/launchdarkly/resource_launchdarkly_custom_role.go index 0fc0686d..e40b3d89 100644 --- a/launchdarkly/resource_launchdarkly_custom_role.go +++ b/launchdarkly/resource_launchdarkly_custom_role.go @@ -105,8 +105,14 @@ func resourceCustomRoleRead(ctx context.Context, d *schema.ResourceData, metaRaw // Because "policy" is now deprecated in favor of "policy_statements", only set "policy" if it has // already been set by the user. + // TODO: Somehow this seems to also add an empty policystatement of + // policy { + // + actions = [] + // + resources = [] + // } if _, ok := d.GetOk(POLICY); ok { - err = d.Set(POLICY, policiesToResourceData(customRole.Policy)) + policies := policiesToResourceData(customRole.Policy) + err = d.Set(POLICY, policies) } else { err = d.Set(POLICY_STATEMENTS, policyStatementsToResourceData(statementsToStatementReps(customRole.Policy))) } @@ -114,7 +120,7 @@ func resourceCustomRoleRead(ctx context.Context, d *schema.ResourceData, metaRaw if err != nil { return diag.Errorf("could not set policy on custom role with id %q: %v", customRoleID, err) } - return nil + return diags } func resourceCustomRoleUpdate(ctx context.Context, d *schema.ResourceData, metaRaw interface{}) diag.Diagnostics { diff --git a/launchdarkly/resource_launchdarkly_custom_role_test.go b/launchdarkly/resource_launchdarkly_custom_role_test.go index afc6bd78..745e6f85 100644 --- a/launchdarkly/resource_launchdarkly_custom_role_test.go +++ b/launchdarkly/resource_launchdarkly_custom_role_test.go @@ -44,6 +44,18 @@ resource "launchdarkly_custom_role" "test" { resources = ["proj/*:env/staging"] } } +` + testAccCustomRoleCreateWithNotStatements = ` +resource "launchdarkly_custom_role" "test" { + key = "%s" + name = "Custom role - %s" + description = "Don't allow all actions on non-staging environments" + policy_statements { + not_actions = ["*"] + effect = "allow" + not_resources = ["proj/*:env/staging"] + } +} ` testAccCustomRoleUpdateWithStatements = ` resource "launchdarkly_custom_role" "test" { @@ -56,6 +68,18 @@ resource "launchdarkly_custom_role" "test" { resources = ["proj/*:env/production"] } } +` + testAccCustomRoleUpdateWithNotStatements = ` +resource "launchdarkly_custom_role" "test" { + key = "%s" + name = "Updated role - %s" + description= "Don't deny all actions on non production environments" + policy_statements { + not_actions = ["*"] + effect = "deny" + not_resources = ["proj/*:env/production"] + } +} ` ) @@ -123,6 +147,41 @@ func TestAccCustomRole_CreateWithStatements(t *testing.T) { }) } +func TestAccCustomRole_CreateWithNotStatements(t *testing.T) { + key := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_custom_role.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCustomRoleCreateWithNotStatements, key, name), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomRoleExists(resourceName), + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Custom role - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Don't allow all actions on non-staging environments"), + resource.TestCheckResourceAttr(resourceName, "policy.#", "0"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_resources.0", "proj/*:env/staging"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.effect", "allow"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccCustomRole_Update(t *testing.T) { key := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) @@ -194,6 +253,42 @@ func TestAccCustomRole_UpdateWithStatements(t *testing.T) { }) } +func TestAccCustomRole_UpdateWithNotStatements(t *testing.T) { + key := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + name := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resourceName := "launchdarkly_custom_role.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCustomRoleCreateWithStatements, key, name), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomRoleExists(resourceName), + ), + }, + { + Config: fmt.Sprintf(testAccCustomRoleUpdateWithNotStatements, key, name), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomRoleExists(resourceName), + resource.TestCheckResourceAttr(resourceName, KEY, key), + resource.TestCheckResourceAttr(resourceName, NAME, "Updated role - "+name), + resource.TestCheckResourceAttr(resourceName, DESCRIPTION, "Don't deny all actions on non production environments"), + resource.TestCheckResourceAttr(resourceName, "policy.#", "0"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_actions.0", "*"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.not_resources.0", "proj/*:env/production"), + resource.TestCheckResourceAttr(resourceName, "policy_statements.0.effect", "deny"), + ), + }, + }, + }) +} + func testAccCheckCustomRoleExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] From 4c8434b6657b7613cbf856b2b9990911831ed631 Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 19 Jan 2022 11:31:53 +0000 Subject: [PATCH 33/36] Clean up policy statements helper functions (#182) * Clean up policy statements helper functions * Fix nil return bug --- launchdarkly/helper.go | 8 +++ launchdarkly/policy_statements_helper.go | 64 ++++++------------- launchdarkly/policy_statements_helper_test.go | 21 +++++- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/launchdarkly/helper.go b/launchdarkly/helper.go index c3855769..4cb3f6f4 100644 --- a/launchdarkly/helper.go +++ b/launchdarkly/helper.go @@ -79,3 +79,11 @@ func stringSliceToInterfaceSlice(input []string) []interface{} { } return o } + +func interfaceSliceToStringSlice(input []interface{}) []string { + o := make([]string, 0, len(input)) + for _, v := range input { + o = append(o, v.(string)) + } + return o +} diff --git a/launchdarkly/policy_statements_helper.go b/launchdarkly/policy_statements_helper.go index 8eeca87a..55f6655c 100644 --- a/launchdarkly/policy_statements_helper.go +++ b/launchdarkly/policy_statements_helper.go @@ -2,7 +2,6 @@ package launchdarkly import ( "errors" - "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -101,10 +100,6 @@ func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.S statements := make([]ldapi.StatementPost, 0, len(schemaStatements)) for _, stmt := range schemaStatements { statement := stmt.(map[string]interface{}) - err := validatePolicyStatement(statement) - if err != nil { - return statements, err - } s, err := policyStatementFromResourceData(statement) if err != nil { return statements, err @@ -115,43 +110,28 @@ func policyStatementsFromResourceData(schemaStatements []interface{}) ([]ldapi.S } func policyStatementFromResourceData(statement map[string]interface{}) (ldapi.StatementPost, error) { - statementResources := []string{} - statementActions := []string{} - statementNotResources := []string{} - statementNotActions := []string{} + err := validatePolicyStatement(statement) + if err != nil { + return ldapi.StatementPost{}, err + } ret := ldapi.StatementPost{ Effect: statement[EFFECT].(string), } - // Build our policy fields - for _, r := range statement[RESOURCES].([]interface{}) { - statementResources = append(statementResources, r.(string)) - } - for _, a := range statement[ACTIONS].([]interface{}) { - statementActions = append(statementActions, a.(string)) - } - // optional fields - rawNotResources := statement[NOT_RESOURCES].([]interface{}) - for _, n := range rawNotResources { - statementNotResources = append(statementNotResources, n.(string)) + resources := interfaceSliceToStringSlice(statement[RESOURCES].([]interface{})) + if len(resources) > 0 { + ret.SetResources(resources) } - rawNotActions := statement[NOT_ACTIONS].([]interface{}) - for _, n := range rawNotActions { - statementNotActions = append(statementNotActions, n.(string)) + notResources := interfaceSliceToStringSlice(statement[NOT_RESOURCES].([]interface{})) + if len(notResources) > 0 { + ret.SetNotResources(notResources) } - // Add the appropriate fields to the statement - if len(statement[RESOURCES].([]interface{})) > 0 { - ret.Resources = &statementResources - } else if len(statement[NOT_RESOURCES].([]interface{})) > 0 { - ret.NotResources = &statementNotResources - } else { - return ret, fmt.Errorf("please provide either 'resources' or not_resources' for your policy_statement") + actions := interfaceSliceToStringSlice(statement[ACTIONS].([]interface{})) + if len(actions) > 0 { + ret.SetActions(actions) } - if len(statement[ACTIONS].([]interface{})) > 0 { - ret.Actions = &statementActions - } else if len(statement[NOT_ACTIONS].([]interface{})) > 0 { - ret.NotActions = &statementNotActions - } else { - return ret, fmt.Errorf("please provide either 'actions' or not_actions' for your policy_statement") + notActions := interfaceSliceToStringSlice(statement[NOT_ACTIONS].([]interface{})) + if len(notActions) > 0 { + ret.SetNotActions(notActions) } return ret, nil @@ -164,18 +144,10 @@ func policyStatementsToResourceData(statements []ldapi.StatementRep) []interface EFFECT: s.Effect, } if s.Resources != nil && len(*s.Resources) > 0 { - var resources []interface{} - for _, v := range *s.Resources { - resources = append(resources, v) - } - t[RESOURCES] = resources + t[RESOURCES] = stringSliceToInterfaceSlice(*s.Resources) } if s.NotResources != nil && len(*s.NotResources) > 0 { - var notResources []interface{} - for _, v := range *s.NotResources { - notResources = append(notResources, v) - } - t[NOT_RESOURCES] = notResources + t[NOT_RESOURCES] = stringSliceToInterfaceSlice(*s.NotResources) } if s.Actions != nil && len(*s.Actions) > 0 { t[ACTIONS] = stringSliceToInterfaceSlice(*s.Actions) diff --git a/launchdarkly/policy_statements_helper_test.go b/launchdarkly/policy_statements_helper_test.go index 30e7ca26..b898cd6a 100644 --- a/launchdarkly/policy_statements_helper_test.go +++ b/launchdarkly/policy_statements_helper_test.go @@ -70,7 +70,7 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, }, { - name: "not_resource example", + name: "not_resources example", policyStatements: map[string]interface{}{ POLICY_STATEMENTS: []interface{}{ map[string]interface{}{ @@ -88,6 +88,25 @@ func TestPolicyStatementsRoundTripConversion(t *testing.T) { }, }, }, + { + name: "not_actions example", + policyStatements: map[string]interface{}{ + POLICY_STATEMENTS: []interface{}{ + map[string]interface{}{ + RESOURCES: []interface{}{"proj/*:env/production:flag/*"}, + NOT_ACTIONS: []interface{}{"*"}, + EFFECT: "allow", + }, + }, + }, + expected: []ldapi.StatementPost{ + { + Resources: strArrayPtr([]string{"proj/*:env/production:flag/*"}), + NotActions: &statementPostActions, + Effect: "allow", + }, + }, + }, } for _, tc := range testCases { From 50d74b99fbcdca6fd1df5ac2e291743f5b0313fe Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 19 Jan 2022 13:57:31 +0000 Subject: [PATCH 34/36] Add relay proxy config data source (#183) Co-authored-by: Isabelle Miller Co-authored-by: Isabelle Miller --- CHANGELOG.md | 6 +- ..._launchdarkly_relay_proxy_configuration.go | 39 +++++ ...chdarkly_relay_proxy_configuration_test.go | 140 ++++++++++++++++++ launchdarkly/provider.go | 23 +-- ..._launchdarkly_relay_proxy_configuration.go | 13 +- .../d/relay_proxy_configuration.html.markdown | 57 +++++++ 6 files changed, 261 insertions(+), 17 deletions(-) create mode 100644 launchdarkly/data_source_launchdarkly_relay_proxy_configuration.go create mode 100644 launchdarkly/data_source_launchdarkly_relay_proxy_configuration_test.go create mode 100644 website/docs/d/relay_proxy_configuration.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b592fc..d3c253e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [2.4.0] (Unreleased) -FEATURES: +FEATURES: - Added a `launchdarkly_team_members` data source to allow using multiple team members in one data source. @@ -8,9 +8,9 @@ FEATURES: - Added a new `launchdarkly_flag_triggers` resource and data source for managing LaunchDarkly flag triggers. -- Added the `launchdarkly_relay_proxy_configuration` for managing configurations for the Relay Proxy's [automatic configuration](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration#writing-an-inline-policy) feature. +- Added a new `launchdarkly_relay_proxy_configuration` resource and data source for managing configurations for the Relay Proxy's [automatic configuration](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration#writing-an-inline-policy) feature. -- Added a the `launchdarkly_audit_log_subscription` resource and data source for managing LaunchDarkly audit log integration subscriptions. +- Added a new `launchdarkly_audit_log_subscription` resource and data source for managing LaunchDarkly audit log integration subscriptions. ENHANCEMENTS: diff --git a/launchdarkly/data_source_launchdarkly_relay_proxy_configuration.go b/launchdarkly/data_source_launchdarkly_relay_proxy_configuration.go new file mode 100644 index 00000000..e6e0307a --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_relay_proxy_configuration.go @@ -0,0 +1,39 @@ +package launchdarkly + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceRelayProxyConfig() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceRelayProxyRead, + + Schema: map[string]*schema.Schema{ + ID: { + Type: schema.TypeString, + Required: true, + Description: "The Relay Proxy configuration's unique 24 character ID", + }, + NAME: { + Type: schema.TypeString, + Description: "A human-friendly name for the Relay Proxy configuration", + Computed: true, + }, + POLICY: policyStatementsSchema(policyStatementSchemaOptions{required: false}), + DISPLAY_KEY: { + Type: schema.TypeString, + Computed: true, + Description: "The last four characters of the full_key.", + }, + }, + } +} + +func dataSourceRelayProxyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + id := d.Get(ID).(string) + d.SetId(id) + return relayProxyConfigRead(ctx, d, m, true) +} diff --git a/launchdarkly/data_source_launchdarkly_relay_proxy_configuration_test.go b/launchdarkly/data_source_launchdarkly_relay_proxy_configuration_test.go new file mode 100644 index 00000000..35b37094 --- /dev/null +++ b/launchdarkly/data_source_launchdarkly_relay_proxy_configuration_test.go @@ -0,0 +1,140 @@ +package launchdarkly + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + ldapi "github.com/launchdarkly/api-client-go/v7" + "github.com/stretchr/testify/require" +) + +const ( + testAccDataSourceRelayProxyConfig = ` +data "launchdarkly_relay_proxy_configuration" "test" { + id = "%s" +} +` +) + +func TestAccDataSourceRelayProxyConfig_noMatchReturnsError(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + + invalidID := "31e801b0f65c6216806bd53b" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceRelayProxyConfig, invalidID), + ExpectError: regexp.MustCompile(fmt.Sprintf("Relay Proxy configuration with id %q not found", invalidID)), + }, + }, + }) +} + +func TestAccDataSourceRelayProxyConfig_exists(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + + name := "test config" + resourceSpec := "proj/*:env/*" + policy := []ldapi.StatementRep{{ + Resources: &([]string{resourceSpec}), + Actions: &([]string{"*"}), + Effect: "allow", + }} + + post := ldapi.NewRelayAutoConfigPost(name, policy) + config, _, err := client.ld.RelayProxyConfigurationsApi.PostRelayAutoConfig(client.ctx).RelayAutoConfigPost(*post).Execute() + require.NoError(t, err) + + defer testAccDeleteRelayProxyConfig(t, client, config.Id) + + resourceName := "data.launchdarkly_relay_proxy_configuration.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceRelayProxyConfig, config.Id), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, NAME, name), + resource.TestCheckResourceAttr(resourceName, DISPLAY_KEY, config.DisplayKey), + resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.resources.0", resourceSpec), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + ), + }, + }, + }) +} + +func TestAccDataSourceRelayProxyConfig_NotResource(t *testing.T) { + accTest := os.Getenv("TF_ACC") + if accTest == "" { + t.SkipNow() + } + client, err := newClient(os.Getenv(LAUNCHDARKLY_ACCESS_TOKEN), os.Getenv(LAUNCHDARKLY_API_HOST), false) + require.NoError(t, err) + + name := "test config" + resourceSpec := "proj/*:env/*" + policy := []ldapi.StatementRep{{ + NotResources: &([]string{resourceSpec}), + Actions: &([]string{"*"}), + Effect: "allow", + }} + + post := ldapi.NewRelayAutoConfigPost(name, policy) + config, _, err := client.ld.RelayProxyConfigurationsApi.PostRelayAutoConfig(client.ctx).RelayAutoConfigPost(*post).Execute() + require.NoError(t, err) + + defer testAccDeleteRelayProxyConfig(t, client, config.Id) + + resourceName := "data.launchdarkly_relay_proxy_configuration.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccDataSourceRelayProxyConfig, config.Id), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, NAME, name), + resource.TestCheckResourceAttr(resourceName, DISPLAY_KEY, config.DisplayKey), + resource.TestCheckResourceAttr(resourceName, "policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.effect", "allow"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.not_resources.0", resourceSpec), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "policy.0.actions.0", "*"), + ), + }, + }, + }) +} + +func testAccDeleteRelayProxyConfig(t *testing.T, client *Client, id string) { + _, err := client.ld.RelayProxyConfigurationsApi.DeleteRelayAutoConfig(client.ctx, id).Execute() + require.NoError(t, err) +} diff --git a/launchdarkly/provider.go b/launchdarkly/provider.go index 402c0e74..f718b456 100644 --- a/launchdarkly/provider.go +++ b/launchdarkly/provider.go @@ -63,17 +63,18 @@ func Provider() *schema.Provider { "launchdarkly_metric": resourceMetric(), }, DataSourcesMap: map[string]*schema.Resource{ - "launchdarkly_team_member": dataSourceTeamMember(), - "launchdarkly_team_members": dataSourceTeamMembers(), - "launchdarkly_project": dataSourceProject(), - "launchdarkly_environment": dataSourceEnvironment(), - "launchdarkly_feature_flag": dataSourceFeatureFlag(), - "launchdarkly_feature_flag_environment": dataSourceFeatureFlagEnvironment(), - "launchdarkly_webhook": dataSourceWebhook(), - "launchdarkly_segment": dataSourceSegment(), - "launchdarkly_flag_trigger": dataSourceFlagTrigger(), - "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), - "launchdarkly_metric": dataSourceMetric(), + "launchdarkly_team_member": dataSourceTeamMember(), + "launchdarkly_team_members": dataSourceTeamMembers(), + "launchdarkly_project": dataSourceProject(), + "launchdarkly_environment": dataSourceEnvironment(), + "launchdarkly_feature_flag": dataSourceFeatureFlag(), + "launchdarkly_feature_flag_environment": dataSourceFeatureFlagEnvironment(), + "launchdarkly_webhook": dataSourceWebhook(), + "launchdarkly_segment": dataSourceSegment(), + "launchdarkly_flag_trigger": dataSourceFlagTrigger(), + "launchdarkly_audit_log_subscription": dataSourceAuditLogSubscription(), + "launchdarkly_relay_proxy_configuration": dataSourceRelayProxyConfig(), + "launchdarkly_metric": dataSourceMetric(), }, ConfigureContextFunc: providerConfigure, } diff --git a/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go b/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go index 7e4a7aed..8c79813c 100644 --- a/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go +++ b/launchdarkly/resource_launchdarkly_relay_proxy_configuration.go @@ -12,7 +12,7 @@ import ( func resourceRelayProxyConfig() *schema.Resource { return &schema.Resource{ CreateContext: relayProxyConfigCreate, - ReadContext: relayProxyConfigRead, + ReadContext: resourceRelayProxyConfigRead, UpdateContext: relayProxyConfigUpdate, DeleteContext: relayProxyConfigDelete, Importer: &schema.ResourceImporter{ @@ -67,16 +67,23 @@ func relayProxyConfigCreate(ctx context.Context, d *schema.ResourceData, m inter return diag.FromErr(err) } - return relayProxyConfigRead(ctx, d, m) + return resourceRelayProxyConfigRead(ctx, d, m) } -func relayProxyConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { +func resourceRelayProxyConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return relayProxyConfigRead(ctx, d, m, false) +} + +func relayProxyConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}, isDataSource bool) diag.Diagnostics { var diags diag.Diagnostics client := m.(*Client) id := d.Id() proxyConfig, res, err := client.ld.RelayProxyConfigurationsApi.GetRelayProxyConfig(client.ctx, id).Execute() if isStatusNotFound(res) { + if isDataSource { + return diag.Errorf("Relay Proxy configuration with id %q not found.", id) + } log.Printf("[DEBUG] Relay Proxy configuration with id %q not found on LaunchDarkly. Removing from state", id) d.SetId("") return diags diff --git a/website/docs/d/relay_proxy_configuration.html.markdown b/website/docs/d/relay_proxy_configuration.html.markdown new file mode 100644 index 00000000..3e4cdde4 --- /dev/null +++ b/website/docs/d/relay_proxy_configuration.html.markdown @@ -0,0 +1,57 @@ +--- +title: "launchdarkly_relay_proxy_configuration" +description: "Get information about Relay Proxy configurations." +--- + +# launchdarkly_relay_proxy_configuration + +Provides a LaunchDarkly Relay Proxy configuration data source for use with the Relay Proxy's [automatic configuration feature](https://docs.launchdarkly.com/home/relay-proxy/automatic-configuration). + +-> **Note:** Relay Proxy automatic configuration is available to customers on an Enterprise LaunchDarkly plan. To learn more, read about our pricing. To upgrade your plan, [contact LaunchDarkly Sales](https://launchdarkly.com/contact-sales/). + +This data source allows you to retrieve Relay Proxy configuration information from your LaunchDarkly organization. + +-> **Note:** It is not possible for this data source to retrieve your Relay Proxy configuration's unique key. This is because the unique key is only exposed upon creation. If you need to reference the Relay Proxy configuration's unique key in your terraform config, use the `launchdarkly_relay_proxy_configuration` resource instead. + +## Example Usage + +```hcl +resource "launchdarkly_relay_proxy_configuration" "example" { + name = "example-config" + policy { + actions = ["*"] + effect = "allow" + resources = ["proj/*:env/*"] + } +} +``` + +## Argument Reference + +- `id` - (Required) The Relay Proxy configuration's unique 24 character ID. The unique relay proxy ID can be found in the relay proxy edit page URL, which you can locate by clicking the three dot menu on your relay proxy item in the UI and selecting 'Edit configuration': + +``` +https://app.launchdarkly.com/settings/relay//edit +``` + +## Attribute Reference + +In addition to the argument above, the resource exports the following attributes: + +- `name` - The human-readable name for your Relay Proxy configuration. + +- `display_key` - The last 4 characters of the Relay Proxy configuration's unique key. + +- `policy` - The Relay Proxy configuration's rule policy block. This determines what content the Relay Proxy receives. To learn more, read [Understanding policies](https://docs.launchdarkly.com/home/members/role-policies#understanding-policies). + +Relay proxy configuration `policy` blocks are composed of the following arguments: + +- `effect` - Either `allow` or `deny`. This argument defines whether the rule policy allows or denies access to the named resources and actions. + +- `resources` - The list of resource specifiers defining the resources to which the rule policy applies. Either `resources` or `not_resources` must be specified. For a list of available resources read [Understanding resource types and scopes](https://docs.launchdarkly.com/home/account-security/custom-roles/resources#understanding-resource-types-and-scopes). + +- `not_resources` - The list of resource specifiers defining the resources to which the rule policy does not apply. Either `resources` or `not_resources` must be specified. For a list of available resources read [Understanding resource types and scopes](https://docs.launchdarkly.com/home/account-security/custom-roles/resources#understanding-resource-types-and-scopes). + +- `actions` The list of action specifiers defining the actions to which the rule policy applies. Either `actions` or `not_actions` must be specified. For a list of available actions read [Actions reference](https://docs.launchdarkly.com/home/account-security/custom-roles/actions#actions-reference). + +- `not_actions` The list of action specifiers defining the actions to which the rule policy does not apply. Either `actions` or `not_actions` must be specified. For a list of available actions read [Actions reference](https://docs.launchdarkly.com/home/account-security/custom-roles/actions#actions-reference). From 7135b154383c7fe4e4ad7e33b79ea587429cbca4 Mon Sep 17 00:00:00 2001 From: Henry Barrow Date: Wed, 19 Jan 2022 14:01:20 +0000 Subject: [PATCH 35/36] Remove unnecessary attributes from metric data source test config (#184) --- .../data_source_launchdarkly_metric_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/launchdarkly/data_source_launchdarkly_metric_test.go b/launchdarkly/data_source_launchdarkly_metric_test.go index e31fc9a5..316a7fab 100644 --- a/launchdarkly/data_source_launchdarkly_metric_test.go +++ b/launchdarkly/data_source_launchdarkly_metric_test.go @@ -15,14 +15,8 @@ import ( const ( testAccDataSourceMetric = ` data "launchdarkly_metric" "testing" { - key = "%s" + key = "%s" project_key = "%s" - name = "%s" - kind = "pageview" - urls { - kind = "substring" - substring = "foo" - } } ` ) @@ -66,7 +60,6 @@ func TestAccDataSourceMetric_noMatchReturnsError(t *testing.T) { }() metricKey := "nonexistent-metric" - metricName := "Metric Data Source Test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) @@ -74,7 +67,7 @@ func TestAccDataSourceMetric_noMatchReturnsError(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccDataSourceMetric, metricKey, project.Key, metricName), + Config: fmt.Sprintf(testAccDataSourceMetric, metricKey, project.Key), ExpectError: regexp.MustCompile("Error: 404 Not Found"), }, }, @@ -121,7 +114,7 @@ func TestAccDataSourceMetric_exists(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(testAccDataSourceMetric, metricKey, projectKey, metricName), + Config: fmt.Sprintf(testAccDataSourceMetric, metricKey, projectKey), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, KEY), resource.TestCheckResourceAttrSet(resourceName, NAME), From 9ab50f2b5151f07dde777eeafbcc0c7430c98d28 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Wed, 19 Jan 2022 16:34:50 +0100 Subject: [PATCH 36/36] prepare release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c253e0..a6656628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [2.4.0] (Unreleased) +## [2.4.0] (January 19, 2022) FEATURES: