Skip to content

Commit 37c667a

Browse files
danielemarosticaDaniele Marostica
andauthored
feat: add support for legacy projects to miactl project describe (#259)
Co-authored-by: Daniele Marostica <[email protected]>
1 parent 7cff3e7 commit 37c667a

File tree

9 files changed

+273
-136
lines changed

9 files changed

+273
-136
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

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

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

internal/clioptions/clioptions.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type CLIOptions struct {
4444

4545
Revision string
4646
Version string
47+
Branch string
48+
Tag string
4749
DeployType string
4850
NoSemVer bool
4951
TriggerID string
@@ -143,6 +145,14 @@ func (o *CLIOptions) AddVersionFlags(flags *pflag.FlagSet) {
143145
flags.StringVar(&o.Version, "version", "", "the version name of the configuration")
144146
}
145147

148+
func (o *CLIOptions) AddBranchFlags(flags *pflag.FlagSet) {
149+
flags.StringVar(&o.Branch, "branch", "", "the branch name of the configuration")
150+
}
151+
152+
func (o *CLIOptions) AddTagFlags(flags *pflag.FlagSet) {
153+
flags.StringVar(&o.Tag, "tag", "", "the tag name of the configuration")
154+
}
155+
146156
func (o *CLIOptions) AddEnvironmentFlags(flags *pflag.FlagSet) {
147157
flags.StringVar(&o.Environment, "environment", "", "the environment scope for the command")
148158
}

internal/cmd/project/apply.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func validateApplyProjectOptions(options applyProjectOptions) error {
116116
}
117117

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

145-
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref)
145+
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref.EncodedLocationPath())
146146
response, err := client.
147147
Post().
148148
APIPath(endpoint).

internal/cmd/project/describe.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package project
1717

1818
import (
1919
"context"
20+
"errors"
2021
"fmt"
2122
"io"
2223

@@ -31,12 +32,17 @@ const (
3132
describeProjectCmdUsage = "describe"
3233
describeProjectCmdShort = "Describe a Project configuration"
3334
describeProjectCmdLong = `Describe the configuration of the specified Project.`
35+
36+
ErrMultipleIdentifiers = "multiple identifiers specified, please provide only one"
37+
ErrMissingIdentifier = "missing revision/version/branch/tag name, please provide one as argument"
3438
)
3539

3640
type describeProjectOptions struct {
37-
ProjectID string
3841
RevisionName string
3942
VersionName string
43+
BranchName string
44+
TagName string
45+
ProjectID string
4046
OutputFormat string
4147
}
4248

@@ -56,6 +62,8 @@ func DescribeCmd(options *clioptions.CLIOptions) *cobra.Command {
5662
cmdOptions := describeProjectOptions{
5763
RevisionName: options.Revision,
5864
VersionName: options.Version,
65+
BranchName: options.Branch,
66+
TagName: options.Tag,
5967
ProjectID: restConfig.ProjectID,
6068
OutputFormat: options.OutputFormat,
6169
}
@@ -68,6 +76,8 @@ func DescribeCmd(options *clioptions.CLIOptions) *cobra.Command {
6876
options.AddProjectFlags(flags)
6977
options.AddRevisionFlags(flags)
7078
options.AddVersionFlags(flags)
79+
options.AddBranchFlags(flags)
80+
options.AddTagFlags(flags)
7181
options.AddOutputFormatFlag(flags, "json")
7282

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

81-
ref, err := configuration.GetEncodedRef(options.RevisionName, options.VersionName)
91+
ref, err := GetRefFromOptions(options)
8292
if err != nil {
8393
return err
8494
}
8595

86-
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration", options.ProjectID, ref)
96+
endpoint := fmt.Sprintf("/api/backend/projects/%s/%s/configuration/", options.ProjectID, ref.EncodedLocationPath())
8797
response, err := client.
8898
Get().
8999
APIPath(endpoint).
@@ -113,3 +123,46 @@ func describeProject(ctx context.Context, client *client.APIClient, options desc
113123
fmt.Fprintln(writer, string(bytes))
114124
return nil
115125
}
126+
127+
func GetRefFromOptions(options describeProjectOptions) (configuration.Ref, error) {
128+
refType := ""
129+
refName := ""
130+
131+
if len(options.RevisionName) > 0 {
132+
refType = configuration.RevisionRefType
133+
refName = options.RevisionName
134+
}
135+
136+
if len(options.VersionName) > 0 {
137+
if len(refType) > 0 {
138+
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
139+
}
140+
141+
refType = configuration.VersionRefType
142+
refName = options.VersionName
143+
}
144+
145+
if len(options.BranchName) > 0 {
146+
if len(refType) > 0 {
147+
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
148+
}
149+
150+
refType = configuration.BranchRefType
151+
refName = options.BranchName
152+
}
153+
154+
if len(options.TagName) > 0 {
155+
if len(refType) > 0 {
156+
return configuration.Ref{}, errors.New(ErrMultipleIdentifiers)
157+
}
158+
159+
refType = configuration.TagRefType
160+
refName = options.TagName
161+
}
162+
163+
if len(refType) == 0 {
164+
return configuration.Ref{}, errors.New(ErrMissingIdentifier)
165+
}
166+
167+
return configuration.NewRef(refType, refName)
168+
}

internal/cmd/project/describe_test.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,57 @@ func TestDescribeProjectCmd(t *testing.T) {
5858
return false
5959
}),
6060
},
61-
"error missing revision/version": {
61+
"error missing revision/version/branch/tag": {
6262
options: describeProjectOptions{
6363
ProjectID: "test-project",
6464
},
6565
expectError: true,
66-
expectedErrorMsg: "missing revision/version name, please provide one as argument",
66+
expectedErrorMsg: "missing revision/version/branch/tag name, please provide one as argument",
6767
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
6868
return false
6969
}),
7070
},
71-
"error both revision/version specified": {
71+
"error multiple revision/version specified": {
7272
options: describeProjectOptions{
7373
ProjectID: "test-project",
7474
RevisionName: "test-revision",
7575
VersionName: "test-version",
7676
},
7777
expectError: true,
78-
expectedErrorMsg: "both revision and version specified, please provide only one",
78+
expectedErrorMsg: "multiple identifiers specified, please provide only one",
7979
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
8080
return false
8181
}),
8282
},
83+
"error multiple branch/revision specified": {
84+
options: describeProjectOptions{
85+
ProjectID: "test-project",
86+
RevisionName: "test-revision",
87+
BranchName: "test-branch",
88+
},
89+
expectError: true,
90+
expectedErrorMsg: "multiple identifiers specified, please provide only one",
91+
testServer: describeTestServer(t, func(_ http.ResponseWriter, _ *http.Request) bool {
92+
return false
93+
}),
94+
},
95+
"valid project with branch": {
96+
options: describeProjectOptions{
97+
ProjectID: "test-project",
98+
BranchName: "test-json-branch",
99+
OutputFormat: "json",
100+
},
101+
revisionName: "test-revision",
102+
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
103+
if r.URL.Path == "/api/backend/projects/test-project/branches/test-json-branch/configuration/" && r.Method == http.MethodGet {
104+
w.WriteHeader(http.StatusOK)
105+
_, _ = w.Write([]byte(`{"name": "test-project", "branch": "test-json-branch"}`))
106+
return true
107+
}
108+
return false
109+
}),
110+
outputTextJSON: `{"config": {"name": "test-project", "branch": "test-json-branch"}}`,
111+
},
83112
"valid project with revision": {
84113
options: describeProjectOptions{
85114
ProjectID: "test-project",
@@ -88,7 +117,7 @@ func TestDescribeProjectCmd(t *testing.T) {
88117
},
89118
revisionName: "test-revision",
90119
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
91-
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-json-revision/configuration" && r.Method == http.MethodGet {
120+
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-json-revision/configuration/" && r.Method == http.MethodGet {
92121
w.WriteHeader(http.StatusOK)
93122
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-json-revision"}`))
94123
return true
@@ -97,14 +126,30 @@ func TestDescribeProjectCmd(t *testing.T) {
97126
}),
98127
outputTextJSON: `{"config": {"name": "test-project", "revision": "test-json-revision"}}`,
99128
},
129+
"valid project with tag": {
130+
options: describeProjectOptions{
131+
ProjectID: "test-project",
132+
TagName: "test-tag",
133+
OutputFormat: "json",
134+
},
135+
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
136+
if r.URL.Path == "/api/backend/projects/test-project/branches/test-tag/configuration/" && r.Method == http.MethodGet {
137+
w.WriteHeader(http.StatusOK)
138+
_, _ = w.Write([]byte(`{"name": "test-project", "tag": "test-tag"}`))
139+
return true
140+
}
141+
return false
142+
}),
143+
outputTextJSON: `{"config": {"name": "test-project", "tag": "test-tag"}}`,
144+
},
100145
"valid project with version": {
101146
options: describeProjectOptions{
102147
ProjectID: "test-project",
103148
VersionName: "test-version",
104149
OutputFormat: "json",
105150
},
106151
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
107-
if r.URL.Path == "/api/backend/projects/test-project/versions/test-version/configuration" && r.Method == http.MethodGet {
152+
if r.URL.Path == "/api/backend/projects/test-project/versions/test-version/configuration/" && r.Method == http.MethodGet {
108153
w.WriteHeader(http.StatusOK)
109154
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-version"}`))
110155
return true
@@ -120,7 +165,7 @@ func TestDescribeProjectCmd(t *testing.T) {
120165
OutputFormat: "yaml",
121166
},
122167
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
123-
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-yaml-revision/configuration" && r.Method == http.MethodGet {
168+
if r.URL.Path == "/api/backend/projects/test-project/revisions/test-yaml-revision/configuration/" && r.Method == http.MethodGet {
124169
w.WriteHeader(http.StatusOK)
125170
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
126171
return true
@@ -136,7 +181,7 @@ func TestDescribeProjectCmd(t *testing.T) {
136181
OutputFormat: "yaml",
137182
},
138183
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
139-
if r.URL.Path == "/api/backend/projects/test-project/revisions/some%2Frevision/configuration" && r.Method == http.MethodGet {
184+
if r.URL.Path == "/api/backend/projects/test-project/revisions/some%2Frevision/configuration/" && r.Method == http.MethodGet {
140185
w.WriteHeader(http.StatusOK)
141186
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
142187
return true
@@ -152,7 +197,7 @@ func TestDescribeProjectCmd(t *testing.T) {
152197
OutputFormat: "yaml",
153198
},
154199
testServer: describeTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
155-
if r.URL.Path == "/api/backend/projects/test-project/versions/version%2F1.2.3/configuration" && r.Method == http.MethodGet {
200+
if r.URL.Path == "/api/backend/projects/test-project/versions/version%2F1.2.3/configuration/" && r.Method == http.MethodGet {
156201
w.WriteHeader(http.StatusOK)
157202
_, _ = w.Write([]byte(`{"name": "test-project", "revision": "test-yaml-revision"}`))
158203
return true
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright Mia srl
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package configuration
17+
18+
import (
19+
"fmt"
20+
"net/url"
21+
)
22+
23+
type RefTypes map[string]bool
24+
25+
const (
26+
RevisionRefType = "revisions"
27+
VersionRefType = "versions"
28+
BranchRefType = "branches"
29+
TagRefType = "tags"
30+
)
31+
32+
var validRefTypes = RefTypes{RevisionRefType: true, VersionRefType: true, BranchRefType: true, TagRefType: true}
33+
34+
type Ref struct {
35+
refType string
36+
refName string
37+
}
38+
39+
func NewRef(refType, refName string) (Ref, error) {
40+
if !validRefTypes[refType] {
41+
return Ref{}, fmt.Errorf("unknown reference type: %s", refType)
42+
}
43+
if len(refName) == 0 {
44+
return Ref{}, fmt.Errorf("missing reference name, please provide a reference name")
45+
}
46+
return Ref{
47+
refType: refType,
48+
refName: refName,
49+
}, nil
50+
}
51+
52+
// EncodedLocationPath returns the encoded path to be used when fetching configuration data
53+
//
54+
// e.g., "<ConsoleURL>/api/projects/<ProjectID>/<EncodedLocationPath()>/configuration"
55+
func (r Ref) EncodedLocationPath() string {
56+
switch r.refType {
57+
case RevisionRefType, VersionRefType:
58+
return fmt.Sprintf("%s/%s", r.refType, url.PathEscape(r.refName))
59+
case BranchRefType, TagRefType:
60+
// Legacy projects use /branches endpoint only
61+
return fmt.Sprintf("branches/%s", url.PathEscape(r.refName))
62+
default:
63+
return ""
64+
}
65+
}

0 commit comments

Comments
 (0)