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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- added `project describe` command
- added `project apply` command
- added `project describe` command. It supports `--revision`, `--version` flags for enhanced workflow projects and `--branch`, `--tag` flags for standard workflow projects.
- added `project apply` command. It supports `--revision` flag as only ehnanced workflow is supported for project appy

## [v0.19.0] - 2025-06-18

Expand Down
10 changes: 10 additions & 0 deletions internal/clioptions/clioptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type CLIOptions struct {

Revision string
Version string
Branch string
Tag string
DeployType string
NoSemVer bool
TriggerID string
Expand Down Expand Up @@ -143,6 +145,14 @@ func (o *CLIOptions) AddVersionFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.Version, "version", "", "the version name of the configuration")
}

func (o *CLIOptions) AddBranchFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.Branch, "branch", "", "the branch name of the configuration")
}

func (o *CLIOptions) AddTagFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.Tag, "tag", "", "the tag name of the configuration")
}

func (o *CLIOptions) AddEnvironmentFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.Environment, "environment", "", "the environment scope for the command")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/project/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func validateApplyProjectOptions(options applyProjectOptions) error {
}

func applyConfiguration(ctx context.Context, client *client.APIClient, options applyProjectOptions) error {
ref, err := configuration.GetEncodedRevisionRef(options.RevisionName)
ref, err := configuration.NewRef(configuration.RevisionRefType, options.RevisionName)
if err != nil {
return err
}
Expand All @@ -142,7 +142,7 @@ func applyConfiguration(ctx context.Context, client *client.APIClient, options a
return fmt.Errorf("cannot encode project configuration: %w", err)
}

endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref)
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref.EncodedLocationPath())
response, err := client.
Post().
APIPath(endpoint).
Expand Down
59 changes: 56 additions & 3 deletions internal/cmd/project/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package project

import (
"context"
"errors"
"fmt"
"io"

Expand All @@ -31,12 +32,17 @@ const (
describeProjectCmdUsage = "describe"
describeProjectCmdShort = "Describe a Project configuration"
describeProjectCmdLong = `Describe the configuration of the specified Project.`

ErrMultipleIdentifiers = "multiple identifiers specified, please provide only one"
ErrMissingIdentifier = "missing revision/version/branch/tag name, please provide one as argument"
)

type describeProjectOptions struct {
ProjectID string
RevisionName string
VersionName string
BranchName string
TagName string
ProjectID string
OutputFormat string
}

Expand All @@ -56,6 +62,8 @@ func DescribeCmd(options *clioptions.CLIOptions) *cobra.Command {
cmdOptions := describeProjectOptions{
RevisionName: options.Revision,
VersionName: options.Version,
BranchName: options.Branch,
TagName: options.Tag,
ProjectID: restConfig.ProjectID,
OutputFormat: options.OutputFormat,
}
Expand All @@ -68,6 +76,8 @@ func DescribeCmd(options *clioptions.CLIOptions) *cobra.Command {
options.AddProjectFlags(flags)
options.AddRevisionFlags(flags)
options.AddVersionFlags(flags)
options.AddBranchFlags(flags)
options.AddTagFlags(flags)
options.AddOutputFormatFlag(flags, "json")

return cmd
Expand All @@ -78,12 +88,12 @@ func describeProject(ctx context.Context, client *client.APIClient, options desc
return fmt.Errorf("missing project name, please provide a project name as argument")
}

ref, err := configuration.GetEncodedRef(options.RevisionName, options.VersionName)
ref, err := GetRefFromOptions(options)
if err != nil {
return err
}

endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref)
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration/", options.ProjectID, ref.EncodedLocationPath())
response, err := client.
Get().
APIPath(endpoint).
Expand Down Expand Up @@ -113,3 +123,46 @@ func describeProject(ctx context.Context, client *client.APIClient, options desc
fmt.Fprintln(writer, string(bytes))
return nil
}

func GetRefFromOptions(options describeProjectOptions) (configuration.Ref, error) {
refType := ""
refName := ""

if len(options.RevisionName) > 0 {
refType = configuration.RevisionRefType
refName = options.RevisionName
}

if len(options.VersionName) > 0 {
if len(refType) > 0 {
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
}

refType = configuration.VersionRefType
refName = options.VersionName
}

if len(options.BranchName) > 0 {
if len(refType) > 0 {
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
}

refType = configuration.BranchRefType
refName = options.BranchName
}

if len(options.TagName) > 0 {
if len(refType) > 0 {
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
}

refType = configuration.TagRefType
refName = options.TagName
}

if len(refType) == 0 {
return configuration.Ref{}, errors.New(ErrMissingIdentifier)
}

return configuration.NewRef(refType, refName)
}
63 changes: 54 additions & 9 deletions internal/cmd/project/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,57 @@ func TestDescribeProjectCmd(t *testing.T) {
return false
}),
},
"error missing revision/version": {
"error missing revision/version/branch/tag": {
options: describeProjectOptions{
ProjectID: "test-project",
},
expectError: true,
expectedErrorMsg: "missing revision/version name, please provide one as argument",
expectedErrorMsg: "missing revision/version/branch/tag name, please provide one as argument",
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
return false
}),
},
"error both revision/version specified": {
"error multiple revision/version specified": {
options: describeProjectOptions{
ProjectID: "test-project",
RevisionName: "test-revision",
VersionName: "test-version",
},
expectError: true,
expectedErrorMsg: "both revision and version specified, please provide only one",
expectedErrorMsg: "multiple identifiers specified, please provide only one",
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
return false
}),
},
"error multiple branch/revision specified": {
options: describeProjectOptions{
ProjectID: "test-project",
RevisionName: "test-revision",
BranchName: "test-branch",
},
expectError: true,
expectedErrorMsg: "multiple identifiers specified, please provide only one",
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
return false
}),
},
"valid project with branch": {
options: describeProjectOptions{
ProjectID: "test-project",
BranchName: "test-json-branch",
OutputFormat: "json",
},
revisionName: "test-revision",
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/branches/test-json-branch/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "branch": "test-json-branch"}`))
return true
}
return false
}),
outputTextJSON: `{"config": {"name": "test-project", "branch": "test-json-branch"}}`,
},
"valid project with revision": {
options: describeProjectOptions{
ProjectID: "test-project",
Expand All @@ -88,7 +117,7 @@ func TestDescribeProjectCmd(t *testing.T) {
},
revisionName: "test-revision",
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-json-revision/configuration" && r.Method == http.MethodGet {
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-json-revision/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-json-revision"}`))
return true
Expand All @@ -97,14 +126,30 @@ func TestDescribeProjectCmd(t *testing.T) {
}),
outputTextJSON: `{"config": {"name": "test-project", "revision": "test-json-revision"}}`,
},
"valid project with tag": {
options: describeProjectOptions{
ProjectID: "test-project",
TagName: "test-tag",
OutputFormat: "json",
},
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/branches/test-tag/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "tag": "test-tag"}`))
return true
}
return false
}),
outputTextJSON: `{"config": {"name": "test-project", "tag": "test-tag"}}`,
},
"valid project with version": {
options: describeProjectOptions{
ProjectID: "test-project",
VersionName: "test-version",
OutputFormat: "json",
},
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/versions/test-version/configuration" && r.Method == http.MethodGet {
if r.URL.Path == "/api/backend/projects/test-project/versions/test-version/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-version"}`))
return true
Expand All @@ -120,7 +165,7 @@ func TestDescribeProjectCmd(t *testing.T) {
OutputFormat: "yaml",
},
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-yaml-revision/configuration" && r.Method == http.MethodGet {
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-yaml-revision/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
return true
Expand All @@ -136,7 +181,7 @@ func TestDescribeProjectCmd(t *testing.T) {
OutputFormat: "yaml",
},
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/revisions/some%2Frevision/configuration" && r.Method == http.MethodGet {
if r.URL.Path == "/api/backend/projects/test-project/revisions/some%2Frevision/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
return true
Expand All @@ -152,7 +197,7 @@ func TestDescribeProjectCmd(t *testing.T) {
OutputFormat: "yaml",
},
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if r.URL.Path == "/api/backend/projects/test-project/versions/version%2F1.2.3/configuration" && r.Method == http.MethodGet {
if r.URL.Path == "/api/backend/projects/test-project/versions/version%2F1.2.3/configuration/" && r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
return true
Expand Down
65 changes: 65 additions & 0 deletions internal/resources/configuration/ref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Mia srl
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configuration

import (
"fmt"
"net/url"
)

type RefTypes map[string]bool

const (
RevisionRefType = "revisions"
VersionRefType = "versions"
BranchRefType = "branches"
TagRefType = "tags"
)

var validRefTypes = RefTypes{RevisionRefType: true, VersionRefType: true, BranchRefType: true, TagRefType: true}

type Ref struct {
refType string
refName string
}

func NewRef(refType, refName string) (Ref, error) {
if !validRefTypes[refType] {
return Ref{}, fmt.Errorf("unknown reference type: %s", refType)
}
if len(refName) == 0 {
return Ref{}, fmt.Errorf("missing reference name, please provide a reference name")
}
return Ref{
refType: refType,
refName: refName,
}, nil
}

// EncodedLocationPath returns the encoded path to be used when fetching configuration data
//
// e.g., "<ConsoleURL>/api/projects/<ProjectID>/<EncodedLocationPath()>/configuration"
func (r Ref) EncodedLocationPath() string {
switch r.refType {
case RevisionRefType, VersionRefType:
return fmt.Sprintf("%s/%s", r.refType, url.PathEscape(r.refName))
case BranchRefType, TagRefType:
// Legacy projects use /branches endpoint only
return fmt.Sprintf("branches/%s", url.PathEscape(r.refName))
default:
return ""
}
}
Loading
Loading