From aae6a0a83037326ea1dede2d92821474955da281 Mon Sep 17 00:00:00 2001 From: Josiah Witt Date: Thu, 30 Sep 2021 13:28:26 -0400 Subject: [PATCH 1/2] Add missing valid variation types to error message (#70) --- launchdarkly/variations_helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From d8122c21a37bc832322a3ea1590dbbe205ddaac5 Mon Sep 17 00:00:00 2001 From: Isabelle Miller Date: Fri, 8 Oct 2021 10:11:01 -0400 Subject: [PATCH 2/2] prepare 2.1.0 release (#71) 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 * update changelog Co-authored-by: Sunny Co-authored-by: Henry Barrow Co-authored-by: Cliff Tawiah <82856282+ctawiah@users.noreply.github.com> --- CHANGELOG.md | 8 + launchdarkly/approvals_helper.go | 123 ++++++++++++++ launchdarkly/environments_helper.go | 21 ++- launchdarkly/feature_flags_helper.go | 7 + launchdarkly/keys.go | 155 +++++++++--------- .../resource_launchdarkly_environment.go | 22 ++- .../resource_launchdarkly_environment_test.go | 103 ++++++++++++ .../resource_launchdarkly_feature_flag.go | 2 + ...resource_launchdarkly_feature_flag_test.go | 7 +- launchdarkly/resource_launchdarkly_project.go | 20 ++- .../resource_launchdarkly_project_test.go | 74 ++++++++- website/docs/index.html.markdown | 25 +-- website/docs/r/environment.html.markdown | 34 ++++ website/docs/r/project.html.markdown | 22 +++ 14 files changed, 519 insertions(+), 104 deletions(-) create mode 100644 launchdarkly/approvals_helper.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0e01d8..a357484e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [2.1.0] (October 8, 2021) + +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/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..12890b49 100644 --- a/launchdarkly/keys.go +++ b/launchdarkly/keys.go @@ -3,78 +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" + 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_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, }, }, }) 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/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 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.