Skip to content
Draft
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
15 changes: 14 additions & 1 deletion cmd/cycloid/components/config_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func NewComponentConfigGetCommand() *cobra.Command {
Example: "cy config get -p project -e env -c component",
}
cyargs.AddCyContext(cmd)
cyargs.AddStackVersionFlags(cmd)
return cmd
}

Expand All @@ -31,6 +32,18 @@ func getComponentConfig(cmd *cobra.Command, args []string) error {
api := common.NewAPI()
m := middleware.NewMiddleware(api)

config, _, err := m.GetComponentConfig(org, project, env, component)
var tag, branch, hash string
versionID, ok, err := cyargs.GetStackVersionID(cmd)
if err != nil {
return err
}
if !ok {
tag, branch, hash, err = cyargs.ResolveStackVersionArg(cmd, m, org, "")
if err != nil {
return err
}
}

config, _, err := m.GetComponentConfig(org, project, env, component, tag, branch, hash, versionID)
return cyout.PrintWithOptions(cmd, config, err, "failed to fetch config of component '"+component+"'", printer.Options{})
}
2 changes: 1 addition & 1 deletion cmd/cycloid/components/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func createComponent(cmd *cobra.Command, args []string) error {
// Fetch base forms value from current component
var currentConfig = make(models.FormVariables)
if currentComponent.UseCase != "" {
currentConfig, _, err = m.GetComponentConfig(org, project, env, component)
currentConfig, _, err = m.GetComponentConfig(org, project, env, component, "", "", "", 0)
if err != nil {
return cyout.PrintWithOptions(cmd, nil, err, "failed to update component '"+component+"', cannot get current config.", printer.Options{})
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cycloid/components/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func updateComponent(cmd *cobra.Command, args []string) error {

var currentConfig = make(models.FormVariables)
if currentComponent.UseCase != "" {
currentConfig, _, err = m.GetComponentConfig(org, project, env, component)
currentConfig, _, err = m.GetComponentConfig(org, project, env, component, "", "", "", 0)
if err != nil {
return cyout.PrintWithOptions(cmd, nil, err, "failed to update component '"+component+"', cannot get current config.", printer.Options{})
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cycloid/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ type Middleware interface {
GetComponent(org, project, env, component string) (*models.Component, *http.Response, error)
MigrateComponent(org, project, env, component, targetProject, targetEnv, newCanonical, newName string) (*models.Component, *http.Response, error)
DeleteComponent(org, project, env, component string, opts DeleteOptions) (*http.Response, error)
GetComponentConfig(org, project, env, component string) (models.FormVariables, *http.Response, error)
GetComponentConfig(org, project, env, component, versionTag, versionBranch, versionCommitHash string, versionID uint32) (models.FormVariables, *http.Response, error)
GetComponentStackConfig(org, project, env, component, useCase, versionTag, versionBranch, versionCommitHash string) (models.ServiceCatalogConfigs, *http.Response, error)

DeleteRole(org, role string) (*http.Response, error)
Expand Down
77 changes: 77 additions & 0 deletions cmd/cycloid/middleware/offline/component_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package offline_test

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/cycloidio/cycloid-cli/cmd/cycloid/common"
"github.com/cycloidio/cycloid-cli/cmd/cycloid/middleware"
"github.com/cycloidio/cycloid-cli/cmd/cycloid/middleware/offline/mockserver"
)

func TestGetComponentConfig_RawVersionID(t *testing.T) {
var capturedQuery string
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.Header().Set("Content-Type", "application/json")
if strings.HasSuffix(r.URL.Path, "/config") {
capturedQuery = r.URL.RawQuery
}
_, _ = w.Write([]byte(`{"data":{}}`))
}))
defer srv.Close()

api := common.NewAPI(common.WithURL(srv.URL), common.WithToken("test-token"))
m := middleware.NewMiddleware(api)

_, _, err := m.GetComponentConfig("org", "proj", "env", "comp", "", "", "", 14)
require.NoError(t, err)
assert.Equal(t, "service_catalog_source_version_id=14", capturedQuery)
assert.Equal(t, 1, callCount, "raw version ID must not trigger extra API calls")
}

func TestGetComponentConfig_BranchResolvesVersion(t *testing.T) {
var capturedQuery string
srv := mockserver.ComponentConfigServer(t, &capturedQuery)
defer srv.Close()

api := common.NewAPI(common.WithURL(srv.URL), common.WithToken("test-token"))
m := middleware.NewMiddleware(api)

_, _, err := m.GetComponentConfig("myorg", "myproj", "myenv", "mycomp", "", mockserver.BranchName, "", 0)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("service_catalog_source_version_id=%d", mockserver.BranchVersionID), capturedQuery)
}

func TestGetComponentConfig_TagResolvesVersion(t *testing.T) {
var capturedQuery string
srv := mockserver.ComponentConfigServer(t, &capturedQuery)
defer srv.Close()

api := common.NewAPI(common.WithURL(srv.URL), common.WithToken("test-token"))
m := middleware.NewMiddleware(api)

_, _, err := m.GetComponentConfig("myorg", "myproj", "myenv", "mycomp", mockserver.TagName, "", "", 0)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("service_catalog_source_version_id=%d", mockserver.TagVersionID), capturedQuery)
}

func TestGetComponentConfig_AutoResolvesLatestVersion(t *testing.T) {
var capturedQuery string
srv := mockserver.ComponentConfigServer(t, &capturedQuery)
defer srv.Close()

api := common.NewAPI(common.WithURL(srv.URL), common.WithToken("test-token"))
m := middleware.NewMiddleware(api)

_, _, err := m.GetComponentConfig("myorg", "myproj", "myenv", "mycomp", "", "", "", 0)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("service_catalog_source_version_id=%d", mockserver.BranchVersionID), capturedQuery)
}
70 changes: 70 additions & 0 deletions cmd/cycloid/middleware/offline/mockserver/mockserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package mockserver

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// Version IDs and names served by ComponentConfigServer.
const (
BranchVersionID = uint32(99)
TagVersionID = uint32(77)
BranchName = "main"
TagName = "v1.2.3"
StackRef = "myorg:my-stack"
CatalogRepo = "my-catalog-repo"
)

// ComponentConfigServer returns a test server that mocks the full resolution chain
// for GetComponentConfig: GetComponent → GetStack → GetCatalogRepository →
// ListStackVersions → config GET. The capturedQuery pointer receives the raw query
// string of the final config request.
func ComponentConfigServer(t *testing.T, capturedQuery *string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
path := r.URL.Path

writeJSON := func(v any) {
_ = json.NewEncoder(w).Encode(map[string]any{"data": v})
}

switch {
case strings.HasSuffix(path, "/components/mycomp/config"):
*capturedQuery = r.URL.RawQuery
writeJSON(map[string]any{})

case strings.HasSuffix(path, "/components/mycomp"):
writeJSON(map[string]any{
"service_catalog": map[string]any{
"ref": StackRef,
"service_catalog_source_canonical": CatalogRepo,
},
})

case strings.Contains(path, "service_catalogs") && strings.HasSuffix(path, "/versions"):
writeJSON([]map[string]any{
{"id": BranchVersionID, "type": "branch", "name": BranchName, "commit_hash": "abc123"},
{"id": TagVersionID, "type": "tag", "name": TagName, "commit_hash": "def456"},
})

case strings.Contains(path, "service_catalogs"):
writeJSON(map[string]any{
"ref": StackRef,
"service_catalog_source_canonical": CatalogRepo,
})

case strings.Contains(path, "service_catalog_sources"):
writeJSON(map[string]any{
"canonical": CatalogRepo,
"branch": BranchName,
})

default:
http.NotFound(w, r)
}
}))
}
19 changes: 18 additions & 1 deletion cmd/cycloid/middleware/organization_components.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,29 @@ import (
"github.com/cycloidio/cycloid-cli/internal/ptr"
)

func (m *middleware) GetComponentConfig(org, project, env, component string) (models.FormVariables, *http.Response, error) {
func (m *middleware) GetComponentConfig(org, project, env, component, versionTag, versionBranch, versionCommitHash string, versionID uint32) (models.FormVariables, *http.Response, error) {
var result models.FormVariables

if versionID == 0 {
comp, _, err := m.GetComponent(org, project, env, component)
if err != nil {
return nil, nil, fmt.Errorf("failed to get component to resolve stack version: %w", err)
}
versionID, _, err = m.resolveStackVersion(org, *comp.ServiceCatalog.Ref, versionTag, versionBranch, versionCommitHash)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve stack version: %w", err)
}
}

query := url.Values{
"service_catalog_source_version_id": []string{strconv.FormatUint(uint64(versionID), 10)},
}

resp, err := m.GenericRequest(Request{
Method: "GET",
Organization: &org,
Route: []string{"organizations", org, "projects", project, "environments", env, "components", component, "config"},
Query: query,
}, &result)
if err != nil {
return nil, resp, err
Expand Down
2 changes: 1 addition & 1 deletion cmd/cycloid/projects/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func create(cmd *cobra.Command, args []string) error {
return err
}

configRepository, err := cyargs.GetDefaultConfigRepository(cmd)
configRepository, err := cyargs.GetConfigRepository(cmd)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cycloid/projects/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func update(cmd *cobra.Command, args []string) error {
return err
}

configRepository, err := cyargs.GetDefaultConfigRepository(cmd)
configRepository, err := cyargs.GetConfigRepository(cmd)
if err != nil {
return err
}
Expand Down
40 changes: 1 addition & 39 deletions internal/cyargs/config_catalog_repositories.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package cyargs

import (
"fmt"
"slices"

"github.com/spf13/cobra"

"github.com/cycloidio/cycloid-cli/client/models"
"github.com/cycloidio/cycloid-cli/cmd/cycloid/common"
"github.com/cycloidio/cycloid-cli/cmd/cycloid/middleware"
)
Expand Down Expand Up @@ -71,7 +67,7 @@ func CompleteCatalogRepository(cmd *cobra.Command, args []string, toComplete str

func AddConfigRepositoryFlag(cmd *cobra.Command) string {
flagName := "config-repository"
cmd.Flags().String(flagName, "", "canonical of a config repository, if empty will use the default one in the current org.")
cmd.Flags().String(flagName, "", "canonical of a config repository")
cmd.RegisterFlagCompletionFunc(flagName, CompleteConfigRepository)
return flagName
}
Expand Down Expand Up @@ -108,8 +104,6 @@ func CompleteConfigRepository(cmd *cobra.Command, args []string, toComplete stri
return configRepositories, cobra.ShellCompDirectiveNoFileComp
}

// GetConfigRepository return the config repository flag, if empty, will try to return
// the current org default config repository
func GetConfigRepository(cmd *cobra.Command) (string, error) {
configRepository, err := cmd.Flags().GetString("config-repository")
if err != nil {
Expand All @@ -118,35 +112,3 @@ func GetConfigRepository(cmd *cobra.Command) (string, error) {

return configRepository, err
}

func GetDefaultConfigRepository(cmd *cobra.Command) (string, error) {
api := common.NewAPI()
m := middleware.NewMiddleware(api)

org, err := GetOrg(cmd)
if err != nil {
return "", fmt.Errorf("failed to get default config repository, missing org argument: %w", err)
}

configRepository, _ := GetConfigRepository(cmd)
if configRepository != "" {
return configRepository, nil
}

// TODO: This behavior will be pushed to backend
// track issue: https://linear.app/cycloid/issue/BE-807/make-the-createproject-route-use-the-default-catalog-if
catalogRepos, _, err := m.ListConfigRepositories(org)
if err != nil {
return "", fmt.Errorf("failed to get the default config repository: %w", err)
}

index := slices.IndexFunc(catalogRepos, func(c *models.ConfigRepository) bool {
return *c.Default
})
if index == -1 {
docURL := "https://docs.cycloid.io/reference/config-and-catalog-repository/"
return "", fmt.Errorf("error: seems like your org %q does not have a default config repository, please add one using this doc: %q", org, docURL)
}

return *catalogRepos[index].Canonical, nil
}
25 changes: 24 additions & 1 deletion internal/cyargs/stacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cyargs

import (
"fmt"
"strconv"
"strings"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -136,7 +137,7 @@ func GetStack(cmd *cobra.Command) (string, error) {
// backward compatibility but are deprecated and will be removed in a future major release.
func AddStackVersionFlags(cmd *cobra.Command) {
cmd.Flags().String("stack-version", "", "stack version (tag, branch, or commit hash). "+
"Use type prefixes to avoid ambiguity: tag:<name>, branch:<name>, sha:<hash>")
"Use type prefixes to avoid ambiguity: tag:<name>, branch:<name>, sha:<hash>, version:<id>")
cmd.RegisterFlagCompletionFunc("stack-version", CompleteStackVersionUnified)

// Legacy flags — kept for backward compatibility.
Expand Down Expand Up @@ -219,6 +220,10 @@ func ResolveStackVersionArg(cmd *cobra.Command, m middleware.Middleware, org, st
return "", value, "", nil
case "sha", "commit":
return "", "", value, nil
case "version":
return "", "", "", fmt.Errorf(
"--stack-version=version:<id> is only supported by 'cy component config get'; " +
"use tag:<name>, branch:<name>, or sha:<hash> for other commands")
}
// Unknown prefix: fall through and attempt bare resolution on the full value.
}
Expand Down Expand Up @@ -266,6 +271,24 @@ func ResolveStackVersionArg(cmd *cobra.Command, m middleware.Middleware, org, st
}
}

// GetStackVersionID parses --stack-version=version:<id> and returns the numeric catalog
// version ID. Returns (id, true, nil) when that form is used, (0, false, nil) otherwise.
func GetStackVersionID(cmd *cobra.Command) (uint32, bool, error) {
v, err := cmd.Flags().GetString("stack-version")
if err != nil || v == "" {
return 0, false, err
}
idStr, ok := strings.CutPrefix(v, "version:")
if !ok {
return 0, false, nil
}
id64, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
return 0, false, fmt.Errorf("--stack-version=version:<id>: expected a numeric ID, got %q", idStr)
}
return uint32(id64), true, nil
}

// CompleteStackVersionUnified provides prefix-aware completion for --stack-version.
//
// - No prefix typed (e.g. "v1"): lists all versions with type in description.
Expand Down