Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions cloudsmith/resource_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,31 @@ func resourceServiceUpdate(ctx context.Context, d *schema.ResourceData, m interf
if err := waiter(checkerFunc, defaultUpdateTimeout, defaultUpdateInterval); err != nil {
return diag.Errorf("error waiting for service (%s) to be updated: %s", d.Id(), err)
}

// If the rotate_api_key field has changed to a non-empty value, trigger an
// API key refresh for this service account. The value of rotate_api_key
// itself is not sent to the API; it is only used to force a Terraform diff
// and therefore an update. Changing it to an empty value (or removing it)
// does not trigger a rotation.
if d.HasChange("rotate_api_key") {
_, newRaw := d.GetChange("rotate_api_key")
newVal, _ := newRaw.(string)

if newVal != "" {
refreshReq := pc.APIClient.OrgsApi.OrgsServicesRefresh(pc.Auth, org, d.Id())
refreshedService, _, err := pc.APIClient.OrgsApi.OrgsServicesRefreshExecute(refreshReq)
if err != nil {
return diag.Errorf("error rotating service (%s.%s) API key: %s", org, d.Id(), err)
}

// Always set the refreshed key first; redaction is handled separately
// below based on the current value of store_api_key.
d.Set("key", refreshedService.GetKey())
}
}

// Ensure we never persist the API key in state when store_api_key is false,
// regardless of whether a rotation took place in this update.
if !requiredBool(d, "store_api_key") {
d.Set("key", "**redacted**")
}
Expand Down Expand Up @@ -338,6 +363,11 @@ func resourceService() *schema.Resource {
Optional: true,
Default: true,
},
"rotate_api_key": {
Type: schema.TypeString,
Description: "Arbitrary value used to trigger rotation of the service's API key. Change this value to rotate the key for a service account.",
Optional: true,
},
},
}
}
55 changes: 53 additions & 2 deletions cloudsmith/resource_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestAccService_basic(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccServiceCheckExists("cloudsmith_service.test"),
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "team.#"),
resource.TestMatchTypeSetElemNestedAttrs("cloudsmith_service.test", "team.*", map[string]*regexp.Regexp{
resource.TestMatchTypeSetElemNestedAttrs("cloudsmith_service.test", "team.*", map[string]*regexp.Regexp{
"slug": regexp.MustCompile("^tf-test-team-svc(-[^2].*)?$"),
"role": regexp.MustCompile("^Member$"),
}),
Expand Down Expand Up @@ -81,6 +81,32 @@ func TestAccService_basic(t *testing.T) {
resource.TestCheckResourceAttr("cloudsmith_service.test", "key", "**redacted**"),
),
},
{
Config: testAccServiceConfigRotateAPIKeyFirst,
Check: resource.ComposeTestCheckFunc(
testAccServiceCheckExists("cloudsmith_service.test"),
// key should be present in state when store_api_key is true
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
),
},
{
Config: testAccServiceConfigRotateAPIKeySecond,
Check: resource.ComposeTestCheckFunc(
// ensure the resource still exists after rotation
testAccServiceCheckExists("cloudsmith_service.test"),
// key should still be set after rotation; we don't assert the exact value
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
),
},
{
Config: testAccServiceConfigRotateAPIKeyStoreFalse,
Check: resource.ComposeTestCheckFunc(
testAccServiceCheckExists("cloudsmith_service.test"),
// when rotating with store_api_key = false, the key must be redacted in state
resource.TestCheckResourceAttr("cloudsmith_service.test", "store_api_key", "false"),
resource.TestCheckResourceAttr("cloudsmith_service.test", "key", "**redacted**"),
),
},
{
ResourceName: "cloudsmith_service.test",
ImportState: true,
Expand All @@ -93,7 +119,7 @@ func TestAccService_basic(t *testing.T) {
), nil
},
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"key", "store_api_key"},
ImportStateVerifyIgnore: []string{"key", "store_api_key", "rotate_api_key"},
},
},
})
Expand Down Expand Up @@ -173,6 +199,31 @@ resource "cloudsmith_service" "test" {
}
`, os.Getenv("CLOUDSMITH_NAMESPACE"))

var testAccServiceConfigRotateAPIKeyFirst = fmt.Sprintf(`
resource "cloudsmith_service" "test" {
name = "TF Test Service cs"
organization = "%s"
rotate_api_key = "first-rotation"
}
`, os.Getenv("CLOUDSMITH_NAMESPACE"))

var testAccServiceConfigRotateAPIKeySecond = fmt.Sprintf(`
resource "cloudsmith_service" "test" {
name = "TF Test Service cs"
organization = "%s"
rotate_api_key = "second-rotation"
}
`, os.Getenv("CLOUDSMITH_NAMESPACE"))

var testAccServiceConfigRotateAPIKeyStoreFalse = fmt.Sprintf(`
resource "cloudsmith_service" "test" {
name = "TF Test Service cs"
organization = "%s"
store_api_key = false
rotate_api_key = "third-rotation"
}
`, os.Getenv("CLOUDSMITH_NAMESPACE"))

var testAccServiceConfigBasicAddToTeam = fmt.Sprintf(`
resource "cloudsmith_team" "test" {
name = "TF Test Team Svc"
Expand Down
1 change: 1 addition & 0 deletions docs/resources/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The following arguments are supported:
* `role` - (Optional) The service's role in the team. If defined, must be one of `Member` or `Manager`.
* `slug` - (Required) The team the service should be added to.
* `store_api_key` - (Optional) The service's API key to be returned in state. Defaults to `true`. If set to `false`, the "key" value is replaced with `**redacted**`. **NOTE:** This will only be applied to newly created service accounts, **this won't take effect for existing service accounts**.
* `rotate_api_key` - (Optional) Arbitrary string used to trigger rotation of the service's API key. Setting this to a non-empty value or changing it between non-empty values (for example from `first-rotation` to `second-rotation`) will rotate the API key for the service account. Removing this field or setting it back to an empty value will not trigger a rotation.

## Attribute Reference

Expand Down
Loading