diff --git a/pkg/credentials/git_credential.go b/pkg/credentials/git_credential.go index ea1b30a7..a1d08b7e 100644 --- a/pkg/credentials/git_credential.go +++ b/pkg/credentials/git_credential.go @@ -7,5 +7,5 @@ type GitCredential interface { // gitCredential is the embedded struct used for Git credentials. type gitCredential struct { - CredentialType Type `json:"Type" validate:"omitempty,oneof=Anonymous GitHub Reference UsernamePassword"` + CredentialType Type `json:"Type" validate:"omitempty,oneof=Anonymous GitHub Reference UsernamePassword SshKey"` } diff --git a/pkg/credentials/resource.go b/pkg/credentials/resource.go index 9d154164..e7ae55af 100644 --- a/pkg/credentials/resource.go +++ b/pkg/credentials/resource.go @@ -115,6 +115,12 @@ func (r *Resource) UnmarshalJSON(b []byte) error { return err } r.Details = gitHubAppGitCredential + case GitCredentialTypeSshKey: + var sshKeyGitCredential *SshKey + if err := json.Unmarshal(*gitCredentials, &sshKeyGitCredential); err != nil { + return err + } + r.Details = sshKeyGitCredential } return nil diff --git a/pkg/credentials/resource_test.go b/pkg/credentials/resource_test.go index 853db535..09feb0cb 100644 --- a/pkg/credentials/resource_test.go +++ b/pkg/credentials/resource_test.go @@ -147,3 +147,51 @@ func TestResourceWithReferenceAsJSON(t *testing.T) { jsonassert.New(t).Assertf(expectedJSON, string(resourceAsJSON)) } + +func TestResourceWithSshKeyAsJSON(t *testing.T) { + description := internal.GetRandomName() + id := internal.GetRandomName() + name := internal.GetRandomName() + selfLink := internal.GetRandomName() + privateKey := core.NewSensitiveValue(internal.GetRandomName()) + + restrictions := credentials.RepositoryRestrictions{ + Enabled: false, + AllowedRepositories: []string{}, + } + + restrictionsAsJSON, err := json.Marshal(restrictions) + require.NoError(t, err) + require.NotNil(t, restrictionsAsJSON) + + sshKey := credentials.NewSshKey(privateKey) + sshKey.Username = internal.GetRandomName() + sshKey.KeyName = internal.GetRandomName() + + sshKeyAsJSON, err := json.Marshal(sshKey) + require.NoError(t, err) + require.NotNil(t, sshKeyAsJSON) + + resource := credentials.NewResource(name, sshKey) + resource.Description = description + resource.ID = id + resource.Links["Self"] = selfLink + resource.RepositoryRestrictions = &restrictions + + expectedJSON := fmt.Sprintf(`{ + "Description": "%s", + "Details": %s, + "RepositoryRestrictions": %s, + "Id": "%s", + "Name": "%s", + "Links": { + "Self": "%s" + } + }`, description, sshKeyAsJSON, restrictionsAsJSON, id, name, selfLink) + + resourceAsJSON, err := json.Marshal(resource) + require.NoError(t, err) + require.NotNil(t, resourceAsJSON) + + jsonassert.New(t).Assertf(expectedJSON, string(resourceAsJSON)) +} diff --git a/pkg/credentials/service_v2.go b/pkg/credentials/service_v2.go new file mode 100644 index 00000000..16655b8e --- /dev/null +++ b/pkg/credentials/service_v2.go @@ -0,0 +1,69 @@ +package credentials + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/constants" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources" +) + +const templateV2 = "/api/{spaceId}/git-credentials{/id}/v2{?skip,take,name}" + +type CreateGitCredentialResponseV2 struct { + ID string `json:"Id"` +} + +type getGitCredentialByIdResponseV2 struct { + GitCredential *Resource `json:"GitCredential"` +} + +// AddV2 creates a new Git credential and returns the ID of the newly-created credential +func AddV2(client newclient.Client, gitCredential *Resource) (*CreateGitCredentialResponseV2, error) { + if gitCredential == nil { + return nil, internal.CreateInvalidParameterError(constants.OperationAdd, constants.ParameterGitCredential) + } + + return newclient.Add[CreateGitCredentialResponseV2](client, templateV2, gitCredential.SpaceID, gitCredential) +} + +// GetV2 returns a page of Git credentials matching the query +func GetV2(client newclient.Client, spaceID string, query Query) (*resources.Resources[*Resource], error) { + return newclient.GetByQuery[Resource](client, templateV2, spaceID, query) +} + +// GetByIDV2 returns the Git credential or an error +func GetByIDV2(client newclient.Client, spaceID string, ID string) (*Resource, error) { + if internal.IsEmpty(ID) { + return nil, internal.CreateInvalidParameterError(constants.OperationGetByID, constants.ParameterID) + } + + spaceID, err := internal.GetSpaceID(spaceID, client.GetSpaceID()) + if err != nil { + return nil, err + } + + path, err := client.URITemplateCache().Expand(templateV2, map[string]any{ + "spaceId": spaceID, + "id": ID, + }) + if err != nil { + return nil, err + } + + response, err := newclient.Get[getGitCredentialByIdResponseV2](client.HttpSession(), path) + if err != nil { + return nil, err + } + + return response.GitCredential, nil +} + +// UpdateV2 modifies a Git credential +func UpdateV2(client newclient.Client, gitCredential *Resource) error { + if gitCredential == nil { + return internal.CreateInvalidParameterError(constants.OperationUpdate, constants.ParameterGitCredential) + } + + _, err := newclient.Update[Resource](client, templateV2, gitCredential.SpaceID, gitCredential.GetID(), gitCredential) + return err +} diff --git a/pkg/credentials/service_v2_internal_test.go b/pkg/credentials/service_v2_internal_test.go new file mode 100644 index 00000000..c65eec02 --- /dev/null +++ b/pkg/credentials/service_v2_internal_test.go @@ -0,0 +1,52 @@ +package credentials + +import ( + "encoding/json" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/stretchr/testify/require" +) + +func TestTemplateV2Expansion(t *testing.T) { + cache := newclient.NewClient(&newclient.HttpSession{}).URITemplateCache() + + listOrCreate, err := cache.Expand(templateV2, map[string]any{"spaceId": "Spaces-1"}) + require.NoError(t, err) + require.Equal(t, "/api/Spaces-1/git-credentials/v2", listOrCreate) + + byIDOrModify, err := cache.Expand(templateV2, map[string]any{"spaceId": "Spaces-1", "id": "GitCredentials-1"}) + require.NoError(t, err) + require.Equal(t, "/api/Spaces-1/git-credentials/GitCredentials-1/v2", byIDOrModify) + + withQuery, err := cache.Expand(templateV2, map[string]any{"spaceId": "Spaces-1", "name": "foo", "skip": 5, "take": 10}) + require.NoError(t, err) + require.Equal(t, "/api/Spaces-1/git-credentials/v2?skip=5&take=10&name=foo", withQuery) +} + +func TestCreateGitCredentialResponseV2Unmarshal(t *testing.T) { + var response CreateGitCredentialResponseV2 + err := json.Unmarshal([]byte(`{ "Id": "GitCredentials-1" }`), &response) + require.NoError(t, err) + require.Equal(t, "GitCredentials-1", response.ID) +} + +func TestGetGitCredentialByIdResponseV2Unmarshal(t *testing.T) { + inputJSON := `{ + "GitCredential": { + "Id": "GitCredentials-1", + "SpaceId": "Spaces-1", + "Name": "my-credential", + "Details": { "Type": "SshKey", "Username": "git", "PrivateKey": { "HasValue": true } }, + "RepositoryRestrictions": { "Enabled": false, "AllowedRepositories": [] }, + "Links": {} + } + }` + + var response getGitCredentialByIdResponseV2 + err := json.Unmarshal([]byte(inputJSON), &response) + require.NoError(t, err) + require.NotNil(t, response.GitCredential) + require.Equal(t, "GitCredentials-1", response.GitCredential.GetID()) + require.Equal(t, GitCredentialTypeSshKey, response.GitCredential.Details.Type()) +} diff --git a/pkg/credentials/service_v2_test.go b/pkg/credentials/service_v2_test.go new file mode 100644 index 00000000..ea895680 --- /dev/null +++ b/pkg/credentials/service_v2_test.go @@ -0,0 +1,36 @@ +package credentials_test + +import ( + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/constants" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/stretchr/testify/require" +) + +func createClient() newclient.Client { + return newclient.NewClient(&newclient.HttpSession{}) +} + +func TestAddV2NilCredential(t *testing.T) { + response, err := credentials.AddV2(createClient(), nil) + require.Equal(t, internal.CreateInvalidParameterError(constants.OperationAdd, constants.ParameterGitCredential), err) + require.Nil(t, response) +} + +func TestGetByIDV2EmptyID(t *testing.T) { + resource, err := credentials.GetByIDV2(createClient(), "Spaces-1", "") + require.Equal(t, internal.CreateInvalidParameterError(constants.OperationGetByID, constants.ParameterID), err) + require.Nil(t, resource) + + resource, err = credentials.GetByIDV2(createClient(), "Spaces-1", " ") + require.Equal(t, internal.CreateInvalidParameterError(constants.OperationGetByID, constants.ParameterID), err) + require.Nil(t, resource) +} + +func TestUpdateV2NilCredential(t *testing.T) { + err := credentials.UpdateV2(createClient(), nil) + require.Equal(t, internal.CreateInvalidParameterError(constants.OperationUpdate, constants.ParameterGitCredential), err) +} diff --git a/pkg/credentials/ssh_key.go b/pkg/credentials/ssh_key.go new file mode 100644 index 00000000..257ba638 --- /dev/null +++ b/pkg/credentials/ssh_key.go @@ -0,0 +1,30 @@ +package credentials + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/core" +) + +type SshKey struct { + Username string `json:"Username,omitempty"` + PrivateKey *core.SensitiveValue `json:"PrivateKey,omitempty"` + Passphrase *core.SensitiveValue `json:"Passphrase,omitempty"` + KeyName string `json:"KeyName,omitempty"` + PrivateKeyFingerprint string `json:"PrivateKeyFingerprint,omitempty"` + + gitCredential +} + +func NewSshKey(privateKey *core.SensitiveValue) *SshKey { + return &SshKey{ + PrivateKey: privateKey, + gitCredential: gitCredential{ + CredentialType: GitCredentialTypeSshKey, + }, + } +} + +func (s *SshKey) Type() Type { + return s.CredentialType +} + +var _ GitCredential = &SshKey{} diff --git a/pkg/credentials/types.go b/pkg/credentials/types.go index bd3839f5..da114d17 100644 --- a/pkg/credentials/types.go +++ b/pkg/credentials/types.go @@ -7,4 +7,5 @@ const ( GitCredentialTypeGitHubApp = Type("GitHub") GitCredentialTypeReference = Type("Reference") GitCredentialTypeUsernamePassword = Type("UsernamePassword") + GitCredentialTypeSshKey = Type("SshKey") ) diff --git a/pkg/platformhubgitcredential/platform_hub_git_credential.go b/pkg/platformhubgitcredential/platform_hub_git_credential.go index f35dfc45..e134287d 100644 --- a/pkg/platformhubgitcredential/platform_hub_git_credential.go +++ b/pkg/platformhubgitcredential/platform_hub_git_credential.go @@ -64,20 +64,35 @@ func (r *PlatformHubGitCredential) UnmarshalJSON(b []byte) error { r.RepositoryRestrictions = &repositoryRestrictions } - var gitCredentials *json.RawMessage - if rawResource["Details"] != nil { detailsValue := rawResource["Details"] - if err := json.Unmarshal(*detailsValue, &gitCredentials); err != nil { + var properties map[string]*json.RawMessage + if err := json.Unmarshal(*detailsValue, &properties); err != nil { return err } - var usernamePasswordGitCredential *credentials.UsernamePassword - if err := json.Unmarshal(*gitCredentials, &usernamePasswordGitCredential); err != nil { - return err + credentialType := credentials.GitCredentialTypeUsernamePassword + if properties["Type"] != nil { + if err := json.Unmarshal(*properties["Type"], &credentialType); err != nil { + return err + } + } + + switch credentialType { + case credentials.GitCredentialTypeSshKey: + var sshKeyGitCredential *credentials.SshKey + if err := json.Unmarshal(*detailsValue, &sshKeyGitCredential); err != nil { + return err + } + r.Details = sshKeyGitCredential + default: + var usernamePasswordGitCredential *credentials.UsernamePassword + if err := json.Unmarshal(*detailsValue, &usernamePasswordGitCredential); err != nil { + return err + } + r.Details = usernamePasswordGitCredential } - r.Details = usernamePasswordGitCredential } return nil diff --git a/pkg/platformhubgitcredential/platform_hub_git_credential_service_v2.go b/pkg/platformhubgitcredential/platform_hub_git_credential_service_v2.go new file mode 100644 index 00000000..b0126ca7 --- /dev/null +++ b/pkg/platformhubgitcredential/platform_hub_git_credential_service_v2.go @@ -0,0 +1,82 @@ +package platformhubgitcredential + +import ( + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/resources" + "github.com/OctopusDeploy/go-octopusdeploy/v2/uritemplates" +) + +const templateV2 = "/api/platformhub/git-credentials{/id}/v2{?skip,take,name}" + +type CreatePlatformHubGitCredentialResponseV2 struct { + ID string `json:"Id"` +} + +// AddV2 creates a new Platform Hub git credential and returns the ID of the newly-created credential +func AddV2(client newclient.Client, platformHubGitCredential *PlatformHubGitCredential) (*CreatePlatformHubGitCredentialResponseV2, error) { + if platformHubGitCredential == nil { + return nil, internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential") + } + if platformHubGitCredential.Name == "" { + return nil, internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential.Name") + } + + path, err := client.URITemplateCache().Expand(templateV2, map[string]any{}) + if err != nil { + return nil, err + } + + return newclient.Post[CreatePlatformHubGitCredentialResponseV2](client.HttpSession(), path, platformHubGitCredential) +} + +// GetV2 returns a page of Platform Hub git credentials matching the query +func GetV2(client newclient.Client, query credentials.Query) (*resources.Resources[*PlatformHubGitCredential], error) { + values, _ := uritemplates.Struct2map(query) + if values == nil { + values = map[string]any{} + } + + path, err := client.URITemplateCache().Expand(templateV2, values) + if err != nil { + return nil, err + } + + return newclient.Get[resources.Resources[*PlatformHubGitCredential]](client.HttpSession(), path) +} + +// GetByIDV2 returns the Platform Hub git credential or an error +func GetByIDV2(client newclient.Client, id string) (*PlatformHubGitCredential, error) { + if id == "" { + return nil, internal.CreateRequiredParameterIsEmptyOrNilError("id") + } + + path, err := client.URITemplateCache().Expand(templateV2, map[string]any{"id": id}) + if err != nil { + return nil, err + } + + return newclient.Get[PlatformHubGitCredential](client.HttpSession(), path) +} + +// UpdateV2 modifies a Platform Hub git credential +func UpdateV2(client newclient.Client, platformHubGitCredential *PlatformHubGitCredential) error { + if platformHubGitCredential == nil { + return internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential") + } + if platformHubGitCredential.ID == "" { + return internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential.ID") + } + if platformHubGitCredential.Name == "" { + return internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential.Name") + } + + path, err := client.URITemplateCache().Expand(templateV2, map[string]any{"id": platformHubGitCredential.ID}) + if err != nil { + return err + } + + _, err = newclient.Put[PlatformHubGitCredential](client.HttpSession(), path, platformHubGitCredential) + return err +} diff --git a/pkg/platformhubgitcredential/platform_hub_git_credential_v2_test.go b/pkg/platformhubgitcredential/platform_hub_git_credential_v2_test.go new file mode 100644 index 00000000..d692dfc9 --- /dev/null +++ b/pkg/platformhubgitcredential/platform_hub_git_credential_v2_test.go @@ -0,0 +1,79 @@ +package platformhubgitcredential + +import ( + "encoding/json" + "testing" + + "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/credentials" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" + "github.com/stretchr/testify/require" +) + +func createClient() newclient.Client { + return newclient.NewClient(&newclient.HttpSession{}) +} + +func TestTemplateV2Expansion(t *testing.T) { + cache := createClient().URITemplateCache() + + listOrCreate, err := cache.Expand(templateV2, map[string]any{}) + require.NoError(t, err) + require.Equal(t, "/api/platformhub/git-credentials/v2", listOrCreate) + + byIDOrModify, err := cache.Expand(templateV2, map[string]any{"id": "GitCredentials-1"}) + require.NoError(t, err) + require.Equal(t, "/api/platformhub/git-credentials/GitCredentials-1/v2", byIDOrModify) +} + +func TestPlatformHubGitCredentialUnmarshalUsernamePassword(t *testing.T) { + inputJSON := `{ + "Id": "GitCredentials-1", + "Name": "user-pass", + "Details": { "Type": "UsernamePassword", "Username": "admin", "Password": { "HasValue": true } }, + "RepositoryRestrictions": { "Enabled": false, "AllowedRepositories": [] }, + "Links": {} + }` + + var resource PlatformHubGitCredential + require.NoError(t, json.Unmarshal([]byte(inputJSON), &resource)) + + require.Equal(t, credentials.GitCredentialTypeUsernamePassword, resource.Details.Type()) + _, ok := resource.Details.(*credentials.UsernamePassword) + require.True(t, ok) +} + +func TestPlatformHubGitCredentialUnmarshalSshKey(t *testing.T) { + inputJSON := `{ + "Id": "GitCredentials-2", + "Name": "ssh", + "Details": { "Type": "SshKey", "Username": "git", "PrivateKey": { "HasValue": true }, "PrivateKeyFingerprint": "SHA256:abc" }, + "RepositoryRestrictions": { "Enabled": false, "AllowedRepositories": [] }, + "Links": {} + }` + + var resource PlatformHubGitCredential + require.NoError(t, json.Unmarshal([]byte(inputJSON), &resource)) + + require.Equal(t, credentials.GitCredentialTypeSshKey, resource.Details.Type()) + sshKey, ok := resource.Details.(*credentials.SshKey) + require.True(t, ok) + require.Equal(t, "SHA256:abc", sshKey.PrivateKeyFingerprint) +} + +func TestAddV2NilCredential(t *testing.T) { + response, err := AddV2(createClient(), nil) + require.Equal(t, internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential"), err) + require.Nil(t, response) +} + +func TestGetByIDV2EmptyID(t *testing.T) { + resource, err := GetByIDV2(createClient(), "") + require.Equal(t, internal.CreateRequiredParameterIsEmptyOrNilError("id"), err) + require.Nil(t, resource) +} + +func TestUpdateV2NilCredential(t *testing.T) { + err := UpdateV2(createClient(), nil) + require.Equal(t, internal.CreateRequiredParameterIsEmptyOrNilError("platformHubGitCredential"), err) +}