From 0bfb89a59027e21abf78ed7c92ae0348214493cc Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:33:07 -0400 Subject: [PATCH 1/7] Update Makefile to build with correct arch for local development Related to Intel vs Apple Chip Macs. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9643a52..1453f7e 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ NAME=doppler BINARY=terraform-provider-${NAME} # Only used for local development VERSION=0.0.1 -OS_ARCH=darwin_amd64 +OS_ARCH=darwin_$$(uname -m) default: install From 9b875fb9acd950b44c93772c5f687ca05e5cb368 Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:33:23 -0400 Subject: [PATCH 2/7] Add .idea dir to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 173bee0..f578e99 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ override.tf.json .terraformrc terraform.rc terraform + +# IDEs +.idea/ From 9cc0543b596e9f5813e465ab43de3b4813f436ef Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:35:51 -0400 Subject: [PATCH 3/7] Clean up error handling by including nil checks in if statements This change is a logical no-op. --- doppler/api.go | 150 ++++++++++++++---------------- doppler/resource_config.go | 18 ++-- doppler/resource_environment.go | 13 +-- doppler/resource_project.go | 13 +-- doppler/resource_secret.go | 16 ++-- doppler/resource_service_token.go | 23 ++--- 6 files changed, 104 insertions(+), 129 deletions(-) diff --git a/doppler/api.go b/doppler/api.go index 845317b..e3224ac 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -148,8 +148,7 @@ func (client APIClient) PerformRequest(req *http.Request, params []QueryParam) ( if !isSuccess(r.StatusCode) { if contentType := r.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") { var errResponse ErrorResponse - err := json.Unmarshal(body, &errResponse) - if err != nil { + if err := json.Unmarshal(body, &errResponse); err != nil { return response, &APIError{Err: err, Message: "Unable to load response"} } @@ -199,9 +198,9 @@ func (client APIClient) GetComputedSecrets(ctx context.Context, project string, if err != nil { return nil, err } - result, modelErr := ParseComputedSecrets(response.Body) - if modelErr != nil { - return nil, &APIError{Err: modelErr, Message: "Unable to parse secrets"} + result, err := ParseComputedSecrets(response.Body) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse secrets"} } return result, nil } @@ -220,9 +219,8 @@ func (client APIClient) GetSecret(ctx context.Context, project string, config st return nil, err } var result Secret - jsonErr := json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse secret"} + if err := json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse secret"} } return &result, nil } @@ -245,11 +243,11 @@ func (client APIClient) UpdateSecrets(ctx context.Context, project string, confi if config != "" { payload["config"] = config } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return &APIError{Err: jsonErr, Message: "Unable to parse secrets"} + body, err := json.Marshal(payload) + if err != nil { + return &APIError{Err: err, Message: "Unable to parse secrets"} } - _, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/configs/config/secrets", []QueryParam{}, body) + _, err = client.PerformRequestWithRetry(ctx, "POST", "/v3/configs/config/secrets", []QueryParam{}, body) if err != nil { return err } @@ -267,9 +265,8 @@ func (client APIClient) GetProject(ctx context.Context, name string) (*Project, return nil, err } var result ProjectResponse - jsonErr := json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse project"} + if err := json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project"} } return &result.Project, nil } @@ -282,18 +279,17 @@ func (client APIClient) CreateProject(ctx context.Context, name string, descript if description != "" { payload["description"] = description } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize project"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize project"} } response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/projects", []QueryParam{}, body) if err != nil { return nil, err } var result ProjectResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse project"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project"} } return &result.Project, nil } @@ -305,18 +301,19 @@ func (client APIClient) UpdateProject(ctx context.Context, currentName string, n "description": description, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize project"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize project"} } + response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/projects/project", []QueryParam{}, body) if err != nil { return nil, err } + var result ProjectResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse project"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project"} } return &result.Project, nil } @@ -325,17 +322,20 @@ func (client APIClient) DeleteProject(ctx context.Context, name string) error { payload := map[string]interface{}{ "project": name, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return &APIError{Err: jsonErr, Message: "Unable to serialize project"} + body, err := json.Marshal(payload) + if err != nil { + return &APIError{Err: err, Message: "Unable to serialize project"} } - _, err := client.PerformRequestWithRetry(ctx, "DELETE", "/v3/projects/project", []QueryParam{}, body) + + _, err = client.PerformRequestWithRetry(ctx, "DELETE", "/v3/projects/project", []QueryParam{}, body) if err != nil { return err } + return nil } +// Integrations // Environments func (client APIClient) GetEnvironment(ctx context.Context, project string, name string) (*Environment, error) { @@ -348,9 +348,8 @@ func (client APIClient) GetEnvironment(ctx context.Context, project string, name return nil, err } var result EnvironmentResponse - jsonErr := json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse environment"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse environment"} } return &result.Environment, nil } @@ -361,18 +360,19 @@ func (client APIClient) CreateEnvironment(ctx context.Context, project string, s "name": name, "slug": slug, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize environment"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize environment"} } + response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/environments", []QueryParam{}, body) if err != nil { return nil, err } + var result EnvironmentResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse environment"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse environment"} } return &result.Environment, nil } @@ -387,18 +387,17 @@ func (client APIClient) RenameEnvironment(ctx context.Context, project string, c "name": newName, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize environment"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize environment"} } response, err := client.PerformRequestWithRetry(ctx, "PUT", "/v3/environments/environment", params, body) if err != nil { return nil, err } var result EnvironmentResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse project"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse project"} } return &result.Environment, nil } @@ -427,9 +426,8 @@ func (client APIClient) GetConfig(ctx context.Context, project string, name stri return nil, err } var result ConfigResponse - jsonErr := json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse config"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse config"} } return &result.Config, nil } @@ -440,18 +438,17 @@ func (client APIClient) CreateConfig(ctx context.Context, project string, enviro "environment": environment, "name": name, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize config"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize config"} } response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/configs", []QueryParam{}, body) if err != nil { return nil, err } var result ConfigResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse config"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse config"} } return &result.Config, nil } @@ -463,18 +460,17 @@ func (client APIClient) RenameConfig(ctx context.Context, project string, curren "name": newName, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize config"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize config"} } response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/configs/config", []QueryParam{}, body) if err != nil { return nil, err } var result ConfigResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse config"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse config"} } return &result.Config, nil } @@ -485,11 +481,11 @@ func (client APIClient) DeleteConfig(ctx context.Context, project string, name s "config": name, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return &APIError{Err: jsonErr, Message: "Unable to serialize config"} + body, err := json.Marshal(payload) + if err != nil { + return &APIError{Err: err, Message: "Unable to serialize config"} } - _, err := client.PerformRequestWithRetry(ctx, "DELETE", "/v3/configs/config", []QueryParam{}, body) + _, err = client.PerformRequestWithRetry(ctx, "DELETE", "/v3/configs/config", []QueryParam{}, body) if err != nil { return err } @@ -508,9 +504,8 @@ func (client APIClient) GetServiceTokens(ctx context.Context, project string, co return nil, err } var result ServiceTokenListResponse - jsonErr := json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse service tokens"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse service tokens"} } return result.ServiceTokens, nil } @@ -522,18 +517,17 @@ func (client APIClient) CreateServiceToken(ctx context.Context, project string, "access": access, "name": name, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to serialize service token"} + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize service token"} } response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/configs/config/tokens", []QueryParam{}, body) if err != nil { return nil, err } var result ServiceTokenResponse - jsonErr = json.Unmarshal(response.Body, &result) - if jsonErr != nil { - return nil, &APIError{Err: jsonErr, Message: "Unable to parse service token"} + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse service token"} } return &result.ServiceToken, nil } @@ -545,11 +539,11 @@ func (client APIClient) DeleteServiceToken(ctx context.Context, project string, "slug": slug, } - body, jsonErr := json.Marshal(payload) - if jsonErr != nil { - return &APIError{Err: jsonErr, Message: "Unable to serialize config"} + body, err := json.Marshal(payload) + if err != nil { + return &APIError{Err: err, Message: "Unable to serialize config"} } - _, err := client.PerformRequestWithRetry(ctx, "DELETE", "/v3/configs/config/tokens/token", []QueryParam{}, body) + _, err = client.PerformRequestWithRetry(ctx, "DELETE", "/v3/configs/config/tokens/token", []QueryParam{}, body) if err != nil { return err } diff --git a/doppler/resource_config.go b/doppler/resource_config.go index 533d5e3..1146b37 100644 --- a/doppler/resource_config.go +++ b/doppler/resource_config.go @@ -87,19 +87,16 @@ func resourceConfigRead(ctx context.Context, d *schema.ResourceData, m interface return diag.FromErr(err) } - setErr := d.Set("project", config.Project) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("project", config.Project); err != nil { + return diag.FromErr(err) } - setErr = d.Set("environment", config.Environment) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("environment", config.Environment); err != nil { + return diag.FromErr(err) } - setErr = d.Set("name", config.Name) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("name", config.Name); err != nil { + return diag.FromErr(err) } return diags @@ -114,8 +111,7 @@ func resourceConfigDelete(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } - err = client.DeleteConfig(ctx, project, name) - if err != nil { + if err = client.DeleteConfig(ctx, project, name); err != nil { return diag.FromErr(err) } diff --git a/doppler/resource_environment.go b/doppler/resource_environment.go index 9dc7be3..deafe01 100644 --- a/doppler/resource_environment.go +++ b/doppler/resource_environment.go @@ -87,14 +87,12 @@ func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } - setErr := d.Set("slug", environment.Slug) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("slug", environment.Slug); err != nil { + return diag.FromErr(err) } - setErr = d.Set("name", environment.Name) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("name", environment.Name); err != nil { + return diag.FromErr(err) } return diags @@ -109,8 +107,7 @@ func resourceEnvironmentDelete(ctx context.Context, d *schema.ResourceData, m in return diag.FromErr(err) } - err = client.DeleteEnvironment(ctx, project, slug) - if err != nil { + if err = client.DeleteEnvironment(ctx, project, slug); err != nil { return diag.FromErr(err) } diff --git a/doppler/resource_project.go b/doppler/resource_project.go index 2489b06..ae4eae0 100644 --- a/doppler/resource_project.go +++ b/doppler/resource_project.go @@ -73,14 +73,12 @@ func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interfac return diag.FromErr(err) } - setErr := d.Set("name", project.Name) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("name", project.Name); err != nil { + return diag.FromErr(err) } - setErr = d.Set("description", project.Description) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("description", project.Description); err != nil { + return diag.FromErr(err) } return diags @@ -92,8 +90,7 @@ func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, m interf var diags diag.Diagnostics name := d.Id() - err := client.DeleteProject(ctx, name) - if err != nil { + if err := client.DeleteProject(ctx, name); err != nil { return diag.FromErr(err) } diff --git a/doppler/resource_secret.go b/doppler/resource_secret.go index f709cad..d14acc6 100644 --- a/doppler/resource_secret.go +++ b/doppler/resource_secret.go @@ -60,8 +60,7 @@ func resourceSecretUpdate(ctx context.Context, d *schema.ResourceData, m interfa value := d.Get("value").(string) if value != "" { newSecret := RawSecret{Name: name, Value: &value} - err := client.UpdateSecrets(ctx, project, config, []RawSecret{newSecret}) - if err != nil { + if err := client.UpdateSecrets(ctx, project, config, []RawSecret{newSecret}); err != nil { return diag.FromErr(err) } } @@ -89,14 +88,12 @@ func resourceSecretRead(ctx context.Context, d *schema.ResourceData, m interface return diag.FromErr(err) } - setErr := d.Set("value", secret.Value.Raw) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("value", secret.Value.Raw); err != nil { + return diag.FromErr(err) } - setErr = d.Set("computed", secret.Value.Computed) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("computed", secret.Value.Computed); err != nil { + return diag.FromErr(err) } return diags @@ -118,8 +115,7 @@ func resourceSecretDelete(ctx context.Context, d *schema.ResourceData, m interfa name := tokens[2] newSecret := RawSecret{Name: name, Value: nil} - err := client.UpdateSecrets(ctx, project, config, []RawSecret{newSecret}) - if err != nil { + if err := client.UpdateSecrets(ctx, project, config, []RawSecret{newSecret}); err != nil { return diag.FromErr(err) } diff --git a/doppler/resource_service_token.go b/doppler/resource_service_token.go index a1d741f..72a3905 100644 --- a/doppler/resource_service_token.go +++ b/doppler/resource_service_token.go @@ -68,9 +68,8 @@ func resourceServiceTokenCreate(ctx context.Context, d *schema.ResourceData, m i d.SetId(token.getResourceId()) - setErr := d.Set("key", token.Key) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("key", token.Key); err != nil { + return diag.FromErr(err) } return diags @@ -102,19 +101,16 @@ func resourceServiceTokenRead(ctx context.Context, d *schema.ResourceData, m int return diag.Errorf("Could not find service token") } - setErr := d.Set("project", token.Project) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("project", token.Project); err != nil { + return diag.FromErr(err) } - setErr = d.Set("config", token.Config) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("config", token.Config); err != nil { + return diag.FromErr(err) } - setErr = d.Set("access", token.Access) - if setErr != nil { - return diag.FromErr(setErr) + if err = d.Set("access", token.Access); err != nil { + return diag.FromErr(err) } // `key` cannot be read after initial creation @@ -131,8 +127,7 @@ func resourceServiceTokenDelete(ctx context.Context, d *schema.ResourceData, m i return diag.FromErr(err) } - err = client.DeleteServiceToken(ctx, project, config, slug) - if err != nil { + if err = client.DeleteServiceToken(ctx, project, config, slug); err != nil { return diag.FromErr(err) } From c8cb26ab70599e60f34a4b6ebb361eaf818c9cdc Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:36:31 -0400 Subject: [PATCH 4/7] Add support for isRetryableAfterSec error flag This flag allows the Doppler backend to specify a retry wait time via the data response object. Retry-after behavior was already supported for rate limit errors but not for general errors. --- doppler/api.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doppler/api.go b/doppler/api.go index e3224ac..3f5d70d 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -66,7 +66,7 @@ func isSuccess(statusCode int) bool { return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399) } -func getSecondsDuration(seconds int64) *time.Duration { +func getSecondsDuration(seconds int) *time.Duration { duration := time.Duration(seconds) * time.Second return &duration } @@ -156,12 +156,15 @@ func (client APIClient) PerformRequest(req *http.Request, params []QueryParam) ( if errResponse.Data["isRetryable"] == true { // Retry immediately retryAfter = getSecondsDuration(0) + } else if retryableAfterSec, ok := errResponse.Data["isRetryableAfterSec"].(float64); ok { + // Retry after specified time + retryAfter = getSecondsDuration(int(retryableAfterSec)) } else if r.StatusCode == 429 { retryAfterStr := r.Header.Get("retry-after") - retryAfterInt, retryAfterErr := strconv.ParseInt(retryAfterStr, 10, 64) - if retryAfterErr == nil { + retryAfterInt, err := strconv.ParseInt(retryAfterStr, 10, 64) + if err == nil { // Parse successful `retry-after` header result - retryAfter = getSecondsDuration(retryAfterInt) + retryAfter = getSecondsDuration(int(retryAfterInt)) } else { // There was some issue parsing, this shouldn't happen but retry after 1 second retryAfter = getSecondsDuration(1) From 52dc19ea44ec80355af29109f5e414bb768e2b56 Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:37:52 -0400 Subject: [PATCH 5/7] Add integration resource --- doppler/api.go | 77 ++++++++++++++++ doppler/models.go | 12 +++ doppler/provider.go | 4 + doppler/resource_integration.go | 128 ++++++++++++++++++++++++++ doppler/resource_integration_types.go | 30 ++++++ 5 files changed, 251 insertions(+) create mode 100644 doppler/resource_integration.go create mode 100644 doppler/resource_integration_types.go diff --git a/doppler/api.go b/doppler/api.go index 3f5d70d..44cd14f 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -339,6 +339,83 @@ func (client APIClient) DeleteProject(ctx context.Context, name string) error { } // Integrations + +func (client APIClient) GetIntegration(ctx context.Context, slug string) (*Integration, error) { + params := []QueryParam{ + {Key: "integration", Value: slug}, + } + response, err := client.PerformRequestWithRetry(ctx, "GET", "/v3/integrations/integration", params, nil) + if err != nil { + return nil, err + } + var result IntegrationResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse integration"} + } + return &result.Integration, nil +} + +func (client APIClient) CreateIntegration(ctx context.Context, data IntegrationData, name, integType string) (*Integration, error) { + payload := map[string]interface{}{ + "name": name, + "type": integType, + "data": data, + } + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize integration"} + } + response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/integrations", []QueryParam{}, body) + if err != nil { + return nil, err + } + + var result IntegrationResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse integration"} + } + return &result.Integration, nil +} + +func (client APIClient) UpdateIntegration(ctx context.Context, slug, name string, data IntegrationData) (*Integration, error) { + params := []QueryParam{ + {Key: "integration", Value: slug}, + } + + payload := map[string]interface{}{} + if name != "" { + payload["name"] = name + } + if data != nil { + payload["data"] = data + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize integration"} + } + response, err := client.PerformRequestWithRetry(ctx, "PUT", "/v3/integrations/integration", params, body) + if err != nil { + return nil, err + } + var result IntegrationResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse integration"} + } + return &result.Integration, nil +} + +func (client APIClient) DeleteIntegration(ctx context.Context, name string) error { + params := []QueryParam{ + {Key: "integration", Value: name}, + } + _, err := client.PerformRequestWithRetry(ctx, "DELETE", "/v3/integrations/integration", params, nil) + if err != nil { + return err + } + return nil +} + // Environments func (client APIClient) GetEnvironment(ctx context.Context, project string, name string) (*Environment, error) { diff --git a/doppler/models.go b/doppler/models.go index 386fdf1..794536c 100644 --- a/doppler/models.go +++ b/doppler/models.go @@ -68,6 +68,18 @@ type ProjectResponse struct { Project Project `json:"project"` } +type IntegrationData = map[string]interface{} + +type Integration struct { + Slug string `json:"slug"` + Name string `json:"name"` + Type string `json:"type"` +} + +type IntegrationResponse struct { + Integration Integration `json:"integration"` +} + type Environment struct { Slug string `json:"slug"` Name string `json:"name"` diff --git a/doppler/provider.go b/doppler/provider.go index ee5fa82..6a15bdf 100644 --- a/doppler/provider.go +++ b/doppler/provider.go @@ -37,6 +37,10 @@ func Provider() *schema.Provider { "doppler_environment": resourceEnvironment(), "doppler_config": resourceConfig(), "doppler_service_token": resourceServiceToken(), + + "doppler_integration_aws_secrets_manager": resourceIntegrationAWSAssumeRoleIntegration("aws_secrets_manager"), + + "doppler_integration_aws_parameter_store": resourceIntegrationAWSAssumeRoleIntegration("aws_parameter_store"), }, DataSourcesMap: map[string]*schema.Resource{ "doppler_secrets": dataSourceSecrets(), diff --git a/doppler/resource_integration.go b/doppler/resource_integration.go new file mode 100644 index 0000000..dff3547 --- /dev/null +++ b/doppler/resource_integration.go @@ -0,0 +1,128 @@ +package doppler + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type IntegrationDataBuilderFunc = func(d *schema.ResourceData) IntegrationData + +type ResourceIntegrationBuilder struct { + Type string + DataSchema map[string]*schema.Schema + DataBuilder IntegrationDataBuilderFunc +} + +// resourceIntegration returns a schema resource object for the integration model. +func (builder ResourceIntegrationBuilder) Build() *schema.Resource { + resourceSchema := map[string]*schema.Schema{ + "name": { + Description: "The name of the integration", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + } + + for name, subschema := range builder.DataSchema { + s := *subschema + resourceSchema[name] = &s + } + + return &schema.Resource{ + CreateContext: builder.CreateContextFunc(), + ReadContext: builder.ReadContextFunc(), + UpdateContext: builder.UpdateContextFunc(), + DeleteContext: builder.DeleteContextFunc(), + Schema: resourceSchema, + } +} + +func (builder ResourceIntegrationBuilder) CreateContextFunc() schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + name := d.Get("name").(string) + integData := builder.DataBuilder(d) + + integ, err := client.CreateIntegration(ctx, integData, name, builder.Type) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(integ.Slug) + + return diags + } +} + +func (builder ResourceIntegrationBuilder) UpdateContextFunc() schema.UpdateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + slug := d.Id() + name := "" + if d.HasChange("name") { + name = d.Get("name").(string) + } + + var data IntegrationData = nil + hasAnyDataFieldChanged := false + + for key := range builder.DataSchema { + if d.HasChange(key) { + hasAnyDataFieldChanged = true + } + } + + if hasAnyDataFieldChanged { + data = builder.DataBuilder(d) + } + + _, err := client.UpdateIntegration(ctx, slug, name, data) + if err != nil { + return diag.FromErr(err) + } + return diags + } +} + +func (builder ResourceIntegrationBuilder) ReadContextFunc() schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + slug := d.Id() + + integ, err := client.GetIntegration(ctx, slug) + if err != nil { + return diag.FromErr(err) + } + + if err = d.Set("name", integ.Name); err != nil { + return diag.FromErr(err) + } + + return diags + } +} + +func (builder ResourceIntegrationBuilder) DeleteContextFunc() schema.DeleteContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + slug := d.Id() + if err := client.DeleteIntegration(ctx, slug); err != nil { + return diag.FromErr(err) + } + + return diags + } +} diff --git a/doppler/resource_integration_types.go b/doppler/resource_integration_types.go new file mode 100644 index 0000000..c27d2fa --- /dev/null +++ b/doppler/resource_integration_types.go @@ -0,0 +1,30 @@ +package doppler + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func awsAssumeRoleDataSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "assume_role_arn": { + Description: "The ARN of the AWS role for Doppler to assume", + Type: schema.TypeString, + Required: true, + }, + } +} + +func awsAssumeRoleDataBuilder(d *schema.ResourceData) IntegrationData { + return IntegrationData{ + "aws_assume_role_arn": d.Get("assume_role_arn"), + } +} + +func resourceIntegrationAWSAssumeRoleIntegration(integrationType string) *schema.Resource { + builder := ResourceIntegrationBuilder{ + Type: integrationType, + DataSchema: awsAssumeRoleDataSchema(), + DataBuilder: awsAssumeRoleDataBuilder, + } + return builder.Build() +} From ab8bb32681dd612357c3d17617249d8389db6da1 Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 15:38:17 -0400 Subject: [PATCH 6/7] Add secrets sync resource --- doppler/api.go | 57 +++++++++++++++ doppler/models.go | 13 ++++ doppler/provider.go | 6 +- doppler/resource_sync.go | 123 +++++++++++++++++++++++++++++++++ doppler/resource_sync_types.go | 75 ++++++++++++++++++++ 5 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 doppler/resource_sync.go create mode 100644 doppler/resource_sync_types.go diff --git a/doppler/api.go b/doppler/api.go index 44cd14f..5c27e0f 100644 --- a/doppler/api.go +++ b/doppler/api.go @@ -416,6 +416,63 @@ func (client APIClient) DeleteIntegration(ctx context.Context, name string) erro return nil } +// Syncs + +func (client APIClient) GetSync(ctx context.Context, config, project, sync string) (*Sync, error) { + params := []QueryParam{ + {Key: "config", Value: config}, + {Key: "project", Value: project}, + {Key: "sync", Value: sync}, + } + response, err := client.PerformRequestWithRetry(ctx, "GET", "/v3/configs/config/syncs/sync", params, nil) + if err != nil { + return nil, err + } + var result SyncResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse sync"} + } + return &result.Sync, nil +} + +func (client APIClient) CreateSync(ctx context.Context, data SyncData, config, project, integration string) (*Sync, error) { + params := []QueryParam{ + {Key: "config", Value: config}, + {Key: "project", Value: project}, + } + payload := map[string]interface{}{ + "integration": integration, + "data": data, + } + body, err := json.Marshal(payload) + if err != nil { + return nil, &APIError{Err: err, Message: "Unable to serialize sync"} + } + response, err := client.PerformRequestWithRetry(ctx, "POST", "/v3/configs/config/syncs", params, body) + if err != nil { + return nil, err + } + var result SyncResponse + if err = json.Unmarshal(response.Body, &result); err != nil { + return nil, &APIError{Err: err, Message: "Unable to parse sync"} + } + return &result.Sync, nil +} + +func (client APIClient) DeleteSync(ctx context.Context, slug string, deleteTarget bool, config, project string) error { + params := []QueryParam{ + {Key: "config", Value: config}, + {Key: "project", Value: project}, + {Key: "sync", Value: slug}, + {Key: "delete_from_target", Value: strconv.FormatBool(deleteTarget)}, + } + _, err := client.PerformRequestWithRetry(ctx, "DELETE", "/v3/configs/config/syncs/sync", params, nil) + if err != nil { + return err + } + return nil +} + // Environments func (client APIClient) GetEnvironment(ctx context.Context, project string, name string) (*Environment, error) { diff --git a/doppler/models.go b/doppler/models.go index 794536c..7cde85e 100644 --- a/doppler/models.go +++ b/doppler/models.go @@ -80,6 +80,19 @@ type IntegrationResponse struct { Integration Integration `json:"integration"` } +type SyncData = map[string]interface{} + +type Sync struct { + Slug string `json:"slug"` + Project string `json:"project"` + Config string `json:"config"` + Integration string `json:"integration"` +} + +type SyncResponse struct { + Sync Sync `json:"sync"` +} + type Environment struct { Slug string `json:"slug"` Name string `json:"name"` diff --git a/doppler/provider.go b/doppler/provider.go index 6a15bdf..58da437 100644 --- a/doppler/provider.go +++ b/doppler/provider.go @@ -38,9 +38,11 @@ func Provider() *schema.Provider { "doppler_config": resourceConfig(), "doppler_service_token": resourceServiceToken(), - "doppler_integration_aws_secrets_manager": resourceIntegrationAWSAssumeRoleIntegration("aws_secrets_manager"), + "doppler_integration_aws_secrets_manager": resourceIntegrationAWSAssumeRoleIntegration("aws_secrets_manager"), + "doppler_secrets_sync_aws_secrets_manager": resourceSyncAWSSecretsManager(), - "doppler_integration_aws_parameter_store": resourceIntegrationAWSAssumeRoleIntegration("aws_parameter_store"), + "doppler_integration_aws_parameter_store": resourceIntegrationAWSAssumeRoleIntegration("aws_parameter_store"), + "doppler_secrets_sync_aws_parameter_store": resourceSyncAWSParameterStore(), }, DataSourcesMap: map[string]*schema.Resource{ "doppler_secrets": dataSourceSecrets(), diff --git a/doppler/resource_sync.go b/doppler/resource_sync.go new file mode 100644 index 0000000..ecddb01 --- /dev/null +++ b/doppler/resource_sync.go @@ -0,0 +1,123 @@ +package doppler + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type SyncDataBuilderFunc = func(d *schema.ResourceData) SyncData + +type ResourceSyncBuilder struct { + DataSchema map[string]*schema.Schema + DataBuilder IntegrationDataBuilderFunc +} + +// resourceSync returns a schema resource object for the Sync model. +func (builder ResourceSyncBuilder) Build() *schema.Resource { + resourceSchema := map[string]*schema.Schema{ + "integration": { + Description: "The slug of the integration to use for this sync", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project": { + Description: "The name of the Doppler project", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "config": { + Description: "The name of the Doppler config", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + } + + for name, subschema := range builder.DataSchema { + s := *subschema + resourceSchema[name] = &s + } + + return &schema.Resource{ + CreateContext: builder.CreateContextFunc(), + ReadContext: builder.ReadContextFunc(), + DeleteContext: builder.DeleteContextFunc(), + Schema: resourceSchema, + } +} + +func (builder ResourceSyncBuilder) CreateContextFunc() schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + integ := d.Get("integration").(string) + config := d.Get("config").(string) + project := d.Get("project").(string) + syncData := builder.DataBuilder(d) + + sync, err := client.CreateSync(ctx, syncData, config, project, integ) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(sync.Slug) + + return diags + } +} + +func (builder ResourceSyncBuilder) ReadContextFunc() schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + name := d.Id() + config := d.Get("config").(string) + project := d.Get("project").(string) + + sync, err := client.GetSync(ctx, config, project, name) + if err != nil { + return diag.FromErr(err) + } + + if err = d.Set("integration", sync.Integration); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("project", sync.Project); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("config", sync.Config); err != nil { + return diag.FromErr(err) + } + + return diags + } +} + +func (builder ResourceSyncBuilder) DeleteContextFunc() schema.DeleteContextFunc { + + return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(APIClient) + + var diags diag.Diagnostics + + slug := d.Id() + config := d.Get("config").(string) + project := d.Get("project").(string) + // In the future, we can support this as a param on the sync + deleteFromTarget := false + if err := client.DeleteSync(ctx, slug, deleteFromTarget, config, project); err != nil { + return diag.FromErr(err) + } + + return diags + } +} diff --git a/doppler/resource_sync_types.go b/doppler/resource_sync_types.go new file mode 100644 index 0000000..4eef25e --- /dev/null +++ b/doppler/resource_sync_types.go @@ -0,0 +1,75 @@ +package doppler + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceSyncAWSSecretsManager() *schema.Resource { + builder := ResourceSyncBuilder{ + DataSchema: map[string]*schema.Schema{ + "region": { + Description: "The AWS region", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": { + Description: "The path to the secret in AWS", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + DataBuilder: func(d *schema.ResourceData) IntegrationData { + return map[string]interface{}{ + "region": d.Get("region"), + "path": d.Get("path"), + } + }, + } + return builder.Build() +} + +func resourceSyncAWSParameterStore() *schema.Resource { + builder := ResourceSyncBuilder{ + DataSchema: map[string]*schema.Schema{ + "region": { + Description: "The AWS region", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "path": { + Description: "The path to the parameters in AWS", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "secure_string": { + Description: "Whether or not the parameters are stored as a secure string", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: true, + }, + "tags": { + Description: "AWS tags to attach to the parameters", + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: true, + }, + }, + DataBuilder: func(d *schema.ResourceData) IntegrationData { + return map[string]interface{}{ + "region": d.Get("region"), + "path": d.Get("path"), + "secure_string": d.Get("secure_string"), + "tags": d.Get("tags"), + } + }, + } + return builder.Build() +} From 88fa9f181ac571df8d97a81347764a8ebb7fba97 Mon Sep 17 00:00:00 2001 From: Nic Manoogian Date: Fri, 24 Mar 2023 16:28:24 -0400 Subject: [PATCH 7/7] Update docs with tfplugindocs --- docs/data-sources/secrets.md | 8 +- docs/index.md | 6 +- docs/resources/config.md | 10 +- docs/resources/environment.md | 10 +- .../integration_aws_parameter_store.md | 93 ++++++++++++++++ .../integration_aws_secrets_manager.md | 87 +++++++++++++++ docs/resources/project.md | 9 +- docs/resources/secret.md | 15 ++- .../secrets_sync_aws_parameter_store.md | 101 ++++++++++++++++++ .../secrets_sync_aws_secrets_manager.md | 90 ++++++++++++++++ docs/resources/service_token.md | 12 +-- .../integration_aws_parameter_store.tf | 67 ++++++++++++ .../integration_aws_secrets_manager.tf | 61 +++++++++++ .../integration_aws_parameter_store.md.tmpl | 16 +++ .../integration_aws_secrets_manager.md.tmpl | 16 +++ .../secrets_sync_aws_parameter_store.md.tmpl | 16 +++ .../secrets_sync_aws_secrets_manager.md.tmpl | 16 +++ 17 files changed, 598 insertions(+), 35 deletions(-) create mode 100644 docs/resources/integration_aws_parameter_store.md create mode 100644 docs/resources/integration_aws_secrets_manager.md create mode 100644 docs/resources/secrets_sync_aws_parameter_store.md create mode 100644 docs/resources/secrets_sync_aws_secrets_manager.md create mode 100644 examples/resources/integration_aws_parameter_store.tf create mode 100644 examples/resources/integration_aws_secrets_manager.tf create mode 100644 templates/resources/integration_aws_parameter_store.md.tmpl create mode 100644 templates/resources/integration_aws_secrets_manager.md.tmpl create mode 100644 templates/resources/secrets_sync_aws_parameter_store.md.tmpl create mode 100644 templates/resources/secrets_sync_aws_secrets_manager.md.tmpl diff --git a/docs/data-sources/secrets.md b/docs/data-sources/secrets.md index f9400ae..e2f8d00 100644 --- a/docs/data-sources/secrets.md +++ b/docs/data-sources/secrets.md @@ -37,10 +37,10 @@ output "json_parsing_values" { ### Optional -- **config** (String) The name of the Doppler config (required for personal tokens) -- **id** (String) The ID of this resource. -- **project** (String) The name of the Doppler project (required for personal tokens) +- `config` (String) The name of the Doppler config (required for personal tokens) +- `project` (String) The name of the Doppler project (required for personal tokens) ### Read-Only -- **map** (Map of String, Sensitive) A mapping of secret names to computed secret values +- `id` (String) The ID of this resource. +- `map` (Map of String, Sensitive) A mapping of secret names to computed secret values diff --git a/docs/index.md b/docs/index.md index e6d0693..beb7b3b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,12 +21,12 @@ provider "doppler" { ### Required -- **doppler_token** (String) A Doppler token, either a personal or service token +- `doppler_token` (String) A Doppler token, either a personal or service token ### Optional -- **host** (String) The Doppler API host (i.e. https://api.doppler.com) -- **verify_tls** (Boolean) Whether or not to verify TLS +- `host` (String) The Doppler API host (i.e. https://api.doppler.com) +- `verify_tls` (Boolean) Whether or not to verify TLS ## Getting Help diff --git a/docs/resources/config.md b/docs/resources/config.md index 92d4e88..1267da4 100644 --- a/docs/resources/config.md +++ b/docs/resources/config.md @@ -24,10 +24,10 @@ resource "doppler_config" "backend_ci_github" { ### Required -- **environment** (String) The name of the Doppler environment where the config is located -- **name** (String) The name of the Doppler config -- **project** (String) The name of the Doppler project where the config is located +- `environment` (String) The name of the Doppler environment where the config is located +- `name` (String) The name of the Doppler config +- `project` (String) The name of the Doppler project where the config is located -### Optional +### Read-Only -- **id** (String) The ID of this resource. +- `id` (String) The ID of this resource. diff --git a/docs/resources/environment.md b/docs/resources/environment.md index 3417960..594f2d4 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -24,10 +24,10 @@ resource "doppler_environment" "backend_ci" { ### Required -- **name** (String) The name of the Doppler environment -- **project** (String) The name of the Doppler project where the environment is located -- **slug** (String) The slug of the Doppler environment +- `name` (String) The name of the Doppler environment +- `project` (String) The name of the Doppler project where the environment is located +- `slug` (String) The slug of the Doppler environment -### Optional +### Read-Only -- **id** (String) The ID of this resource. +- `id` (String) The ID of this resource. diff --git a/docs/resources/integration_aws_parameter_store.md b/docs/resources/integration_aws_parameter_store.md new file mode 100644 index 0000000..41d31ad --- /dev/null +++ b/docs/resources/integration_aws_parameter_store.md @@ -0,0 +1,93 @@ +--- +page_title: "doppler_integration_aws_parameter_store Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Parameter Store Doppler integration. +--- + +# doppler_integration_aws_parameter_store (Resource) + +Manage an AWS Parameter Store Doppler integration. + +## Example Usage + +```terraform +resource "aws_iam_role" "doppler_parameter_store" { + name = "doppler_parameter_store" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "ssm:PutParameter", + "ssm:LabelParameterVersion", + "ssm:DeleteParameter", + "ssm:RemoveTagsFromResource", + "ssm:GetParameterHistory", + "ssm:AddTagsToResource", + "ssm:GetParametersByPath", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:DeleteParameters" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain names + }, + ] + }) + } +} + + +resource "doppler_integration_aws_parameter_store" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_parameter_store.arn +} + +resource "doppler_secrets_sync_aws_parameter_store" "backend_prod" { + integration = doppler_integration_aws_parameter_store.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" + secure_string = true + tags = { myTag = "enabled" } +} +``` + + +## Schema + +### Required + +- `assume_role_arn` (String) The ARN of the AWS role for Doppler to assume +- `name` (String) The name of the integration + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/integration_aws_secrets_manager.md b/docs/resources/integration_aws_secrets_manager.md new file mode 100644 index 0000000..b471038 --- /dev/null +++ b/docs/resources/integration_aws_secrets_manager.md @@ -0,0 +1,87 @@ +--- +page_title: "doppler_integration_aws_secrets_manager Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Secrets Manager Doppler integration. +--- + +# doppler_integration_aws_secrets_manager (Resource) + +Manage an AWS Secrets Manager Doppler integration. + +## Example Usage + +```terraform +resource "aws_iam_role" "doppler_secrets_manager" { + name = "doppler_secrets_manager" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:PutSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:TagResource", + "secretsmanager:UpdateSecret" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain secret names + }, + ] + }) + } +} + +resource "doppler_integration_aws_secrets_manager" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_secrets_manager.arn +} + +resource "doppler_secrets_sync_aws_secrets_manager" "backend_prod" { + integration = doppler_integration_aws_secrets_manager.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" +} +``` + + +## Schema + +### Required + +- `assume_role_arn` (String) The ARN of the AWS role for Doppler to assume +- `name` (String) The name of the integration + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/project.md b/docs/resources/project.md index 9091b31..5418693 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -23,9 +23,12 @@ resource "doppler_project" "backend" { ### Required -- **name** (String) The name of the Doppler project +- `name` (String) The name of the Doppler project ### Optional -- **description** (String) The description of the Doppler project -- **id** (String) The ID of this resource. +- `description` (String) The description of the Doppler project + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/secret.md b/docs/resources/secret.md index 59a166a..74ec7bf 100644 --- a/docs/resources/secret.md +++ b/docs/resources/secret.md @@ -36,15 +36,12 @@ output "resource_value" { ### Required -- **config** (String) The name of the Doppler config -- **name** (String) The name of the Doppler secret -- **project** (String) The name of the Doppler project -- **value** (String, Sensitive) The raw secret value - -### Optional - -- **id** (String) The ID of this resource. +- `config` (String) The name of the Doppler config +- `name` (String) The name of the Doppler secret +- `project` (String) The name of the Doppler project +- `value` (String, Sensitive) The raw secret value ### Read-Only -- **computed** (String, Sensitive) The computed secret value, after resolving secret references +- `computed` (String, Sensitive) The computed secret value, after resolving secret references +- `id` (String) The ID of this resource. diff --git a/docs/resources/secrets_sync_aws_parameter_store.md b/docs/resources/secrets_sync_aws_parameter_store.md new file mode 100644 index 0000000..988a62d --- /dev/null +++ b/docs/resources/secrets_sync_aws_parameter_store.md @@ -0,0 +1,101 @@ +--- +page_title: "doppler_secrets_sync_aws_parameter_store Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Parameter Store Doppler sync. +--- + +# doppler_secrets_sync_aws_parameter_store (Resource) + +Manage an AWS Parameter Store Doppler sync. + +## Example Usage + +```terraform +resource "aws_iam_role" "doppler_parameter_store" { + name = "doppler_parameter_store" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "ssm:PutParameter", + "ssm:LabelParameterVersion", + "ssm:DeleteParameter", + "ssm:RemoveTagsFromResource", + "ssm:GetParameterHistory", + "ssm:AddTagsToResource", + "ssm:GetParametersByPath", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:DeleteParameters" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain names + }, + ] + }) + } +} + + +resource "doppler_integration_aws_parameter_store" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_parameter_store.arn +} + +resource "doppler_secrets_sync_aws_parameter_store" "backend_prod" { + integration = doppler_integration_aws_parameter_store.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" + secure_string = true + tags = { myTag = "enabled" } +} +``` + + +## Schema + +### Required + +- `config` (String) The name of the Doppler config +- `integration` (String) The slug of the integration to use for this sync +- `path` (String) The path to the parameters in AWS +- `project` (String) The name of the Doppler project +- `region` (String) The AWS region + +### Optional + +- `secure_string` (Boolean) Whether or not the parameters are stored as a secure string +- `tags` (Map of String) AWS tags to attach to the parameters + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/secrets_sync_aws_secrets_manager.md b/docs/resources/secrets_sync_aws_secrets_manager.md new file mode 100644 index 0000000..b4bc3e8 --- /dev/null +++ b/docs/resources/secrets_sync_aws_secrets_manager.md @@ -0,0 +1,90 @@ +--- +page_title: "doppler_secrets_sync_aws_secrets_manager Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Secrets Manager Doppler sync. +--- + +# doppler_secrets_sync_aws_secrets_manager (Resource) + +Manage an AWS Secrets Manager Doppler sync. + +## Example Usage + +```terraform +resource "aws_iam_role" "doppler_secrets_manager" { + name = "doppler_secrets_manager" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:PutSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:TagResource", + "secretsmanager:UpdateSecret" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain secret names + }, + ] + }) + } +} + +resource "doppler_integration_aws_secrets_manager" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_secrets_manager.arn +} + +resource "doppler_secrets_sync_aws_secrets_manager" "backend_prod" { + integration = doppler_integration_aws_secrets_manager.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" +} +``` + + +## Schema + +### Required + +- `config` (String) The name of the Doppler config +- `integration` (String) The slug of the integration to use for this sync +- `path` (String) The path to the secret in AWS +- `project` (String) The name of the Doppler project +- `region` (String) The AWS region + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/service_token.md b/docs/resources/service_token.md index 5ba694a..e3807ab 100644 --- a/docs/resources/service_token.md +++ b/docs/resources/service_token.md @@ -27,15 +27,15 @@ resource "doppler_service_token" "backend_ci_token" { ### Required -- **config** (String) The name of the Doppler config where the service token is located -- **name** (String) The name of the Doppler service token -- **project** (String) The name of the Doppler project where the service token is located +- `config` (String) The name of the Doppler config where the service token is located +- `name` (String) The name of the Doppler service token +- `project` (String) The name of the Doppler project where the service token is located ### Optional -- **access** (String) The access level (read or read/write) -- **id** (String) The ID of this resource. +- `access` (String) The access level (read or read/write) ### Read-Only -- **key** (String, Sensitive) The key for the Doppler service token +- `id` (String) The ID of this resource. +- `key` (String, Sensitive) The key for the Doppler service token diff --git a/examples/resources/integration_aws_parameter_store.tf b/examples/resources/integration_aws_parameter_store.tf new file mode 100644 index 0000000..3c1719d --- /dev/null +++ b/examples/resources/integration_aws_parameter_store.tf @@ -0,0 +1,67 @@ +resource "aws_iam_role" "doppler_parameter_store" { + name = "doppler_parameter_store" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "ssm:PutParameter", + "ssm:LabelParameterVersion", + "ssm:DeleteParameter", + "ssm:RemoveTagsFromResource", + "ssm:GetParameterHistory", + "ssm:AddTagsToResource", + "ssm:GetParametersByPath", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:DeleteParameters" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain names + }, + ] + }) + } +} + + +resource "doppler_integration_aws_parameter_store" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_parameter_store.arn +} + +resource "doppler_secrets_sync_aws_parameter_store" "backend_prod" { + integration = doppler_integration_aws_parameter_store.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" + secure_string = true + tags = { myTag = "enabled" } +} + diff --git a/examples/resources/integration_aws_secrets_manager.tf b/examples/resources/integration_aws_secrets_manager.tf new file mode 100644 index 0000000..074403e --- /dev/null +++ b/examples/resources/integration_aws_secrets_manager.tf @@ -0,0 +1,61 @@ +resource "aws_iam_role" "doppler_secrets_manager" { + name = "doppler_secrets_manager" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::299900769157:user/doppler-integration-operator" + }, + Condition = { + StringEquals = { + "sts:ExternalId" = "" + } + } + }, + ] + }) + + inline_policy { + name = "doppler_secret_manager" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:PutSecretValue", + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:TagResource", + "secretsmanager:UpdateSecret" + ] + Effect = "Allow" + Resource = "*" + # Limit Doppler to only access certain secret names + }, + ] + }) + } +} + +resource "doppler_integration_aws_secrets_manager" "prod" { + name = "Production" + assume_role_arn = aws_iam_role.doppler_secrets_manager.arn +} + +resource "doppler_secrets_sync_aws_secrets_manager" "backend_prod" { + integration = doppler_integration_aws_secrets_manager.prod.id + project = "backend" + config = "prd" + + region = "us-east-1" + path = "/backend/" +} + diff --git a/templates/resources/integration_aws_parameter_store.md.tmpl b/templates/resources/integration_aws_parameter_store.md.tmpl new file mode 100644 index 0000000..2071121 --- /dev/null +++ b/templates/resources/integration_aws_parameter_store.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "doppler_integration_aws_parameter_store Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Parameter Store Doppler integration. +--- + +# doppler_integration_aws_parameter_store (Resource) + +Manage an AWS Parameter Store Doppler integration. + +## Example Usage + +{{tffile "examples/resources/integration_aws_parameter_store.tf"}} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/integration_aws_secrets_manager.md.tmpl b/templates/resources/integration_aws_secrets_manager.md.tmpl new file mode 100644 index 0000000..6609904 --- /dev/null +++ b/templates/resources/integration_aws_secrets_manager.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "doppler_integration_aws_secrets_manager Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Secrets Manager Doppler integration. +--- + +# doppler_integration_aws_secrets_manager (Resource) + +Manage an AWS Secrets Manager Doppler integration. + +## Example Usage + +{{tffile "examples/resources/integration_aws_secrets_manager.tf"}} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/secrets_sync_aws_parameter_store.md.tmpl b/templates/resources/secrets_sync_aws_parameter_store.md.tmpl new file mode 100644 index 0000000..322ce7b --- /dev/null +++ b/templates/resources/secrets_sync_aws_parameter_store.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "doppler_secrets_sync_aws_parameter_store Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Parameter Store Doppler sync. +--- + +# doppler_secrets_sync_aws_parameter_store (Resource) + +Manage an AWS Parameter Store Doppler sync. + +## Example Usage + +{{tffile "examples/resources/integration_aws_parameter_store.tf"}} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/secrets_sync_aws_secrets_manager.md.tmpl b/templates/resources/secrets_sync_aws_secrets_manager.md.tmpl new file mode 100644 index 0000000..4d97622 --- /dev/null +++ b/templates/resources/secrets_sync_aws_secrets_manager.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "doppler_secrets_sync_aws_secrets_manager Resource - terraform-provider-doppler" +subcategory: "" +description: |- + Manage an AWS Secrets Manager Doppler sync. +--- + +# doppler_secrets_sync_aws_secrets_manager (Resource) + +Manage an AWS Secrets Manager Doppler sync. + +## Example Usage + +{{tffile "examples/resources/integration_aws_secrets_manager.tf"}} + +{{ .SchemaMarkdown | trimspace }}