Skip to content

Commit 5c0f7a7

Browse files
add ability to rotate service api key
1 parent 8f51679 commit 5c0f7a7

File tree

3 files changed

+105
-3
lines changed

3 files changed

+105
-3
lines changed

cloudsmith/resource_service.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,31 @@ func resourceServiceUpdate(ctx context.Context, d *schema.ResourceData, m interf
221221
if err := waiter(checkerFunc, defaultUpdateTimeout, defaultUpdateInterval); err != nil {
222222
return diag.Errorf("error waiting for service (%s) to be updated: %s", d.Id(), err)
223223
}
224-
if !requiredBool(d, "store_api_key") {
224+
225+
// If the rotate_api_key field has changed to a non-empty value, trigger an
226+
// API key refresh for this service account. The value of rotate_api_key
227+
// itself is not sent to the API; it is only used to force a Terraform diff
228+
// and therefore an update. Changing it to an empty value (or removing it)
229+
// does not trigger a rotation.
230+
if d.HasChange("rotate_api_key") {
231+
_, newRaw := d.GetChange("rotate_api_key")
232+
newVal, _ := newRaw.(string)
233+
234+
if newVal != "" {
235+
refreshReq := pc.APIClient.OrgsApi.OrgsServicesRefresh(pc.Auth, org, d.Id())
236+
refreshedService, _, err := pc.APIClient.OrgsApi.OrgsServicesRefreshExecute(refreshReq)
237+
if err != nil {
238+
return diag.Errorf("error rotating service (%s.%s) API key: %s", org, d.Id(), err)
239+
}
240+
241+
if requiredBool(d, "store_api_key") {
242+
d.Set("key", refreshedService.GetKey())
243+
} else {
244+
d.Set("key", "**redacted**")
245+
}
246+
}
247+
} else if !requiredBool(d, "store_api_key") {
248+
// Ensure we never persist the API key in state when store_api_key is false.
225249
d.Set("key", "**redacted**")
226250
}
227251
return resourceServiceRead(ctx, d, m)
@@ -338,6 +362,11 @@ func resourceService() *schema.Resource {
338362
Optional: true,
339363
Default: true,
340364
},
365+
"rotate_api_key": {
366+
Type: schema.TypeString,
367+
Description: "Arbitrary value used to trigger rotation of the service's API key. Change this value to rotate the key for a service account.",
368+
Optional: true,
369+
},
341370
},
342371
}
343372
}

cloudsmith/resource_service_test.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func TestAccService_basic(t *testing.T) {
5050
Check: resource.ComposeTestCheckFunc(
5151
testAccServiceCheckExists("cloudsmith_service.test"),
5252
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "team.#"),
53-
resource.TestMatchTypeSetElemNestedAttrs("cloudsmith_service.test", "team.*", map[string]*regexp.Regexp{
53+
resource.TestMatchTypeSetElemNestedAttrs("cloudsmith_service.test", "team.*", map[string]*regexp.Regexp{
5454
"slug": regexp.MustCompile("^tf-test-team-svc(-[^2].*)?$"),
5555
"role": regexp.MustCompile("^Member$"),
5656
}),
@@ -81,6 +81,23 @@ func TestAccService_basic(t *testing.T) {
8181
resource.TestCheckResourceAttr("cloudsmith_service.test", "key", "**redacted**"),
8282
),
8383
},
84+
{
85+
Config: testAccServiceConfigRotateAPIKeyFirst,
86+
Check: resource.ComposeTestCheckFunc(
87+
testAccServiceCheckExists("cloudsmith_service.test"),
88+
// key should be present in state when store_api_key is true
89+
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
90+
),
91+
},
92+
{
93+
Config: testAccServiceConfigRotateAPIKeySecond,
94+
Check: resource.ComposeTestCheckFunc(
95+
// ensure the resource still exists after rotation
96+
testAccServiceCheckExists("cloudsmith_service.test"),
97+
// key should still be set after rotation; we don't assert the value
98+
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
99+
),
100+
},
84101
{
85102
ResourceName: "cloudsmith_service.test",
86103
ImportState: true,
@@ -93,7 +110,46 @@ func TestAccService_basic(t *testing.T) {
93110
), nil
94111
},
95112
ImportStateVerify: true,
96-
ImportStateVerifyIgnore: []string{"key", "store_api_key"},
113+
ImportStateVerifyIgnore: []string{"key", "store_api_key", "rotate_api_key"},
114+
},
115+
},
116+
})
117+
}
118+
119+
// TestAccService_rotate focuses specifically on exercising the rotate_api_key
120+
// trigger to ensure that rotating a service account's API key works without
121+
// involving team assignments or import behaviour.
122+
func TestAccService_rotate(t *testing.T) {
123+
t.Parallel()
124+
125+
resource.Test(t, resource.TestCase{
126+
PreCheck: func() { testAccPreCheck(t) },
127+
Providers: testAccProviders,
128+
CheckDestroy: testAccServiceCheckDestroy("cloudsmith_service.test"),
129+
Steps: []resource.TestStep{
130+
{
131+
Config: testAccServiceConfigBasic,
132+
Check: resource.ComposeTestCheckFunc(
133+
testAccServiceCheckExists("cloudsmith_service.test"),
134+
// initial key should be present when store_api_key is true (default)
135+
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
136+
),
137+
},
138+
{
139+
Config: testAccServiceConfigRotateAPIKeyFirst,
140+
Check: resource.ComposeTestCheckFunc(
141+
testAccServiceCheckExists("cloudsmith_service.test"),
142+
// key should still be set after first rotation
143+
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
144+
),
145+
},
146+
{
147+
Config: testAccServiceConfigRotateAPIKeySecond,
148+
Check: resource.ComposeTestCheckFunc(
149+
testAccServiceCheckExists("cloudsmith_service.test"),
150+
// key should remain set after subsequent rotations
151+
resource.TestCheckResourceAttrSet("cloudsmith_service.test", "key"),
152+
),
97153
},
98154
},
99155
})
@@ -173,6 +229,22 @@ resource "cloudsmith_service" "test" {
173229
}
174230
`, os.Getenv("CLOUDSMITH_NAMESPACE"))
175231

232+
var testAccServiceConfigRotateAPIKeyFirst = fmt.Sprintf(`
233+
resource "cloudsmith_service" "test" {
234+
name = "TF Test Service cs"
235+
organization = "%s"
236+
rotate_api_key = "first-rotation"
237+
}
238+
`, os.Getenv("CLOUDSMITH_NAMESPACE"))
239+
240+
var testAccServiceConfigRotateAPIKeySecond = fmt.Sprintf(`
241+
resource "cloudsmith_service" "test" {
242+
name = "TF Test Service cs"
243+
organization = "%s"
244+
rotate_api_key = "second-rotation"
245+
}
246+
`, os.Getenv("CLOUDSMITH_NAMESPACE"))
247+
176248
var testAccServiceConfigBasicAddToTeam = fmt.Sprintf(`
177249
resource "cloudsmith_team" "test" {
178250
name = "TF Test Team Svc"

docs/resources/service.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The following arguments are supported:
4242
* `role` - (Optional) The service's role in the team. If defined, must be one of `Member` or `Manager`.
4343
* `slug` - (Required) The team the service should be added to.
4444
* `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**.
45+
* `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 `rotation-one` to `rotation-two`) 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.
4546

4647
## Attribute Reference
4748

0 commit comments

Comments
 (0)