From 7ef0e105bf86c6e79c375f4f68f7dbbe2ec0e094 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Wed, 22 Nov 2023 09:58:18 -0600 Subject: [PATCH] Feat: rotating SSH Key Value causes a replace (with new ID) as opposed to just edit-in-place (#752) --- client/api_client.go | 1 + client/api_client_mock.go | 15 +++++++++++++++ client/sshkey.go | 13 +++++++++++++ client/sshkey_test.go | 33 +++++++++++++++++++++++++++++++++ env0/resource_sshkey.go | 17 ++++++++++++++++- env0/resource_sshkey_test.go | 31 ++++++++++++++++++++++++++++--- 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/client/api_client.go b/client/api_client.go index 06cd2b84..78445ff3 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -40,6 +40,7 @@ type ApiClientInterface interface { VariablesFromRepository(payload *VariablesFromRepositoryPayload) ([]ConfigurationVariable, error) SshKeys() ([]SshKey, error) SshKeyCreate(payload SshKeyCreatePayload) (*SshKey, error) + SshKeyUpdate(id string, payload *SshKeyUpdatePayload) (*SshKey, error) SshKeyDelete(id string) error CredentialsCreate(request CredentialCreatePayload) (Credentials, error) CredentialsUpdate(id string, request CredentialCreatePayload) (Credentials, error) diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 62af3eec..ba717cca 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -1596,6 +1596,21 @@ func (mr *MockApiClientInterfaceMockRecorder) SshKeyDelete(arg0 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SshKeyDelete", reflect.TypeOf((*MockApiClientInterface)(nil).SshKeyDelete), arg0) } +// SshKeyUpdate mocks base method. +func (m *MockApiClientInterface) SshKeyUpdate(arg0 string, arg1 *SshKeyUpdatePayload) (*SshKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SshKeyUpdate", arg0, arg1) + ret0, _ := ret[0].(*SshKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SshKeyUpdate indicates an expected call of SshKeyUpdate. +func (mr *MockApiClientInterfaceMockRecorder) SshKeyUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SshKeyUpdate", reflect.TypeOf((*MockApiClientInterface)(nil).SshKeyUpdate), arg0, arg1) +} + // SshKeys mocks base method. func (m *MockApiClientInterface) SshKeys() ([]SshKey, error) { m.ctrl.T.Helper() diff --git a/client/sshkey.go b/client/sshkey.go index 6096d5c0..2ef6fd24 100644 --- a/client/sshkey.go +++ b/client/sshkey.go @@ -18,6 +18,10 @@ type SshKeyCreatePayload struct { Value string `json:"value"` } +type SshKeyUpdatePayload struct { + Value string `json:"value"` +} + func (client *ApiClient) SshKeyCreate(payload SshKeyCreatePayload) (*SshKey, error) { organizationId, err := client.OrganizationId() if err != nil { @@ -32,6 +36,15 @@ func (client *ApiClient) SshKeyCreate(payload SshKeyCreatePayload) (*SshKey, err return &result, nil } +func (client *ApiClient) SshKeyUpdate(id string, payload *SshKeyUpdatePayload) (*SshKey, error) { + var result SshKey + + if err := client.http.Put("/ssh-keys/"+id, payload, &result); err != nil { + return nil, err + } + return &result, nil +} + func (client *ApiClient) SshKeyDelete(id string) error { return client.http.Delete("/ssh-keys/"+id, nil) } diff --git a/client/sshkey_test.go b/client/sshkey_test.go index a3c8ebac..d7ed1768 100644 --- a/client/sshkey_test.go +++ b/client/sshkey_test.go @@ -80,4 +80,37 @@ var _ = Describe("SshKey", func() { Expect(sshKeys).Should(ContainElement(mockSshKey)) }) }) + + Describe("SshKetUpdate", func() { + Describe("Success", func() { + updateMockSshKey := mockSshKey + updateMockSshKey.Value = "new-value" + var updatedSshKey *SshKey + var err error + + BeforeEach(func() { + updateSshKeyPayload := SshKeyUpdatePayload{Value: updateMockSshKey.Value} + + httpCall = mockHttpClient.EXPECT(). + Put("/ssh-keys/"+mockSshKey.Id, &updateSshKeyPayload, gomock.Any()). + Do(func(path string, request interface{}, response *SshKey) { + *response = updateMockSshKey + }) + + updatedSshKey, err = apiClient.SshKeyUpdate(mockSshKey.Id, &updateSshKeyPayload) + }) + + It("Should send Put request with expected payload", func() { + httpCall.Times(1) + }) + + It("Should not return an error", func() { + Expect(err).To(BeNil()) + }) + + It("Should return ssh key received from API", func() { + Expect(*updatedSshKey).To(Equal(updateMockSshKey)) + }) + }) + }) }) diff --git a/env0/resource_sshkey.go b/env0/resource_sshkey.go index 84c186b7..ab8a1426 100644 --- a/env0/resource_sshkey.go +++ b/env0/resource_sshkey.go @@ -14,6 +14,7 @@ func resourceSshKey() *schema.Resource { return &schema.Resource{ CreateContext: resourceSshKeyCreate, ReadContext: resourceSshKeyRead, + UpdateContext: resourceSshKeyUpdate, DeleteContext: resourceSshKeyDelete, Importer: &schema.ResourceImporter{StateContext: resourceSshKeyImport}, @@ -29,7 +30,6 @@ func resourceSshKey() *schema.Resource { Type: schema.TypeString, Description: "value is a private key in PEM format (first line usually looks like -----BEGIN OPENSSH PRIVATE KEY-----)", Required: true, - ForceNew: true, Sensitive: true, }, }, @@ -54,6 +54,21 @@ func resourceSshKeyCreate(ctx context.Context, d *schema.ResourceData, meta inte return nil } +func resourceSshKeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + var payload client.SshKeyUpdatePayload + if err := readResourceData(&payload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + if _, err := apiClient.SshKeyUpdate(d.Id(), &payload); err != nil { + return diag.Errorf("could not update ssh key: %v", err) + } + + return nil +} + func resourceSshKeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { sshKey, err := getSshKeyById(d.Id(), meta) if err != nil { diff --git a/env0/resource_sshkey_test.go b/env0/resource_sshkey_test.go index 521cc7ae..211fb647 100644 --- a/env0/resource_sshkey_test.go +++ b/env0/resource_sshkey_test.go @@ -12,15 +12,25 @@ func TestUnitSshKeyResource(t *testing.T) { resourceType := "env0_ssh_key" resourceName := "test" accessor := resourceAccessor(resourceType, resourceName) + sshKey := &client.SshKey{ Id: "id0", Name: "name0", Value: "Key🔑", } + + updatedSshKey := *sshKey + updatedSshKey.Value = "new-valuw" + sshKeyCreatePayload := client.SshKeyCreatePayload{ Name: sshKey.Name, Value: sshKey.Value, } + + sshKeyUpdatePayload := client.SshKeyUpdatePayload{ + Value: updatedSshKey.Value, + } + t.Run("Success", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ @@ -35,13 +45,28 @@ func TestUnitSshKeyResource(t *testing.T) { resource.TestCheckResourceAttr(accessor, "value", sshKey.Value), ), }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedSshKey.Name, + "value": updatedSshKey.Value, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", sshKey.Id), + resource.TestCheckResourceAttr(accessor, "name", updatedSshKey.Name), + resource.TestCheckResourceAttr(accessor, "value", updatedSshKey.Value), + ), + }, }, } runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { - mock.EXPECT().SshKeyCreate(sshKeyCreatePayload).Times(1).Return(sshKey, nil) - mock.EXPECT().SshKeys().Times(1).Return([]client.SshKey{*sshKey}, nil) - mock.EXPECT().SshKeyDelete(sshKey.Id).Times(1).Return(nil) + gomock.InOrder( + mock.EXPECT().SshKeyCreate(sshKeyCreatePayload).Times(1).Return(sshKey, nil), + mock.EXPECT().SshKeys().Times(2).Return([]client.SshKey{*sshKey}, nil), + mock.EXPECT().SshKeyUpdate(sshKey.Id, &sshKeyUpdatePayload).Times(1).Return(&updatedSshKey, nil), + mock.EXPECT().SshKeys().Times(1).Return([]client.SshKey{updatedSshKey}, nil), + mock.EXPECT().SshKeyDelete(sshKey.Id).Times(1).Return(nil), + ) }) })