Skip to content
Merged
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
69 changes: 58 additions & 11 deletions cmd/cycloid/credentials/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package credentials
import (
"fmt"
"os"
"slices"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -205,16 +204,14 @@ func create(cmd *cobra.Command, args []string) error {
return err
}

credentialPath, _ := cyargs.GetCredentialPath(cmd)
if credentialPath == "" {
credentialPath = pathFromCanonical(credential)
}

name, _ := cyargs.GetCredentialName(cmd)
if name == "" {
name = credential
}

credentialPath, _ := cyargs.GetCredentialPath(cmd)
credentialPath = defaultCredentialPath(credentialPath, credential, name)

description, err := cyargs.GetCredentialDescription(cmd)
if err != nil {
return err
Expand All @@ -237,12 +234,21 @@ func create(cmd *cobra.Command, args []string) error {
if updateAllowed, _ := cmd.Flags().GetBool("update"); updateAllowed {
credentials, _, err := m.ListCredentials(org, credentialTypes)
if err != nil {
return fmt.Errorf("failed to create --update credential, cannot check for existing credential %q: %w", credential, err)
}
return fmt.Errorf("failed to create --update credential, cannot check for existing credential (canonical=%q path=%q name=%q): %w",
credential, credentialPath, name, err)
}

existingCredential := findCredentialForUpdate(credentials, credential, credentialPath, name)
if existingCredential != nil {
// update requires the canonical in the route: infer it when the user identified
// the credential by path/name (the common create --name --update flow).
if credential == "" && existingCredential.Canonical != nil {
credential = *existingCredential.Canonical
if err := cmd.Flags().Set("canonical", credential); err != nil {
return fmt.Errorf("failed to set credential canonical before update: %w", err)
}
}

if slices.IndexFunc(credentials, func(c *models.CredentialSimple) bool {
return *c.Canonical == credential
}) != -1 {
return update(cmd, args)
}
}
Expand Down Expand Up @@ -454,3 +460,44 @@ func create(cmd *cobra.Command, args []string) error {

return printer.SmartPrint(p, outCredential, nil, "", printer.Options{}, cmd.OutOrStdout())
}

func defaultCredentialPath(path, canonical, name string) string {
if path != "" {
return path
}
if canonical != "" {
return pathFromCanonical(canonical)
}
if name != "" {
return pathFromCanonical(name)
}
return ""
}

func findCredentialForUpdate(credentials []*models.CredentialSimple, canonical, path, name string) *models.CredentialSimple {
if canonical != "" {
for _, credential := range credentials {
if credential != nil && credential.Canonical != nil && *credential.Canonical == canonical {
return credential
}
}
}

if path != "" {
for _, credential := range credentials {
if credential != nil && credential.Path != nil && *credential.Path == path {
return credential
}
}
}

if name != "" {
for _, credential := range credentials {
if credential != nil && credential.Name != nil && *credential.Name == name {
return credential
}
}
}

return nil
}
87 changes: 87 additions & 0 deletions cmd/cycloid/credentials/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package credentials

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/cycloidio/cycloid-cli/client/models"
)

func TestDefaultCredentialPath(t *testing.T) {
t.Run("keep explicit path", func(t *testing.T) {
assert.Equal(t, "explicit-path", defaultCredentialPath("explicit-path", "canonical", "name"))
})

t.Run("fallback to canonical", func(t *testing.T) {
assert.Equal(t, "my-canonical", defaultCredentialPath("", "my-canonical", "name"))
})

t.Run("fallback to name when canonical missing", func(t *testing.T) {
assert.Equal(t, "my-name", defaultCredentialPath("", "", "my-name"))
})

t.Run("CLI-104 name-only derives path like create", func(t *testing.T) {
assert.Equal(t, "testflotmp", defaultCredentialPath("", "", "testflotmp"))
})

t.Run("empty when no identifier", func(t *testing.T) {
assert.Equal(t, "", defaultCredentialPath("", "", ""))
})
}

func TestFindCredentialForUpdate(t *testing.T) {
credentials := []*models.CredentialSimple{
{
Canonical: ptr("cred-canonical"),
Path: ptr("cred-path"),
Name: ptr("cred-name"),
},
{
Canonical: ptr("another-canonical"),
Path: ptr("another-path"),
Name: ptr("another-name"),
},
}

t.Run("match by canonical first", func(t *testing.T) {
got := findCredentialForUpdate(credentials, "cred-canonical", "another-path", "another-name")
assert.NotNil(t, got)
assert.Equal(t, "cred-canonical", *got.Canonical)
})

t.Run("match by path when canonical missing", func(t *testing.T) {
got := findCredentialForUpdate(credentials, "", "cred-path", "")
assert.NotNil(t, got)
assert.Equal(t, "cred-canonical", *got.Canonical)
})

t.Run("match by name when canonical and path missing", func(t *testing.T) {
got := findCredentialForUpdate(credentials, "", "", "cred-name")
assert.NotNil(t, got)
assert.Equal(t, "cred-canonical", *got.Canonical)
})

// CLI-104: `create --name X --update` leaves canonical empty; path defaults from name (e.g. testflotmp).
t.Run("match by path from name defaulting (CLI-104)", func(t *testing.T) {
issue := []*models.CredentialSimple{
{
Canonical: ptr("testflotmp"),
Path: ptr("testflotmp"),
Name: ptr("testflotmp"),
},
}
got := findCredentialForUpdate(issue, "", "testflotmp", "testflotmp")
assert.NotNil(t, got)
assert.Equal(t, "testflotmp", *got.Canonical)
})

t.Run("no match", func(t *testing.T) {
got := findCredentialForUpdate(credentials, "", "missing-path", "missing-name")
assert.Nil(t, got)
})
}

func ptr(value string) *string {
return &value
}
10 changes: 8 additions & 2 deletions cmd/cycloid/middleware/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,15 @@ func (m *middleware) DeleteCredential(org, credential string) (*http.Response, e
}

func (m *middleware) ListCredentials(org, credentialType string) ([]*models.CredentialSimple, *http.Response, error) {
var query url.Values
// Explicit pagination matches swagger defaults (page_size 1000) so callers such as
// `credential create --update` do not miss an existing credential on backends that
// use a smaller default page size.
query := url.Values{
"page_index": []string{"1"},
"page_size": []string{"1000"},
}
if credentialType != "" {
query = url.Values{"credential_type": []string{credentialType}}
query.Set("credential_type", credentialType)
}

var result []*models.CredentialSimple
Expand Down