Skip to content

Commit 75bfcc6

Browse files
feat: setup get component version command (#82)
* feat: setup get component version command this sets up the get component version command. This is a very early PR that we will need to split up into multiple sub PRs with corrections in modules but it serves as an integration point. * chore: fixup oci logger * feat: table output * chore: revert unnecessary changes * chore: add wordlist exclusions * chore: add wordlist exclusions * chore: bump dependencies * chore: add necessary changes for plugin package * chore: add necessary changes for credentials package * chore: add necessary changes for oci package * chore: add necessary changes for cli * chore: fixup * chore: fixup * feat: migrate to plugin powered command * feat: migrate to plugin powered command * chore: fixup * chore: fixup compref test after new defaulting rule * chore: fixup compref test after new defaulting rule * chore: refactor cmdline to be structured * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: more refactor and tests * chore: fixup todo * fix: silence usage on error in cmd * chore: fixup context and doc generation * chore: bump deps
1 parent ee74ead commit 75bfcc6

46 files changed

Lines changed: 2327 additions & 193 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{ "replacementPatterns": [
22
{ "pattern": "^/", "replacement": "{{BASEURL}}/" }
33
],
4-
"ignorePatterns": []
4+
"ignorePatterns": [],
5+
"retryOn429": "true"
56
}

.github/config/wordlist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ cncf
4040
codeowners
4141
codeql
4242
commontransportarchive
43+
commontransportformat
4344
componentaccess
4445
componentaccessimpl
4546
componentarchive
@@ -199,6 +200,7 @@ oci
199200
ociartifact
200201
ocireg
201202
ociregistry
203+
ocirepository
202204
ocm
203205
ocmbot
204206
ocmcli

cli/cmd/cmd.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
10+
"ocm.software/open-component-model/cli/cmd/generate"
11+
"ocm.software/open-component-model/cli/cmd/get"
12+
"ocm.software/open-component-model/cli/configuration/v1"
13+
ocmctx "ocm.software/open-component-model/cli/internal/context"
14+
"ocm.software/open-component-model/cli/log"
15+
)
16+
17+
// Execute adds all child commands to the Cmd command and sets flags appropriately.
18+
// This is called by main.main(). It only needs to happen once to the Cmd.
19+
func Execute() {
20+
err := New().Execute()
21+
if err != nil {
22+
os.Exit(1)
23+
}
24+
}
25+
26+
func New() *cobra.Command {
27+
cmd := &cobra.Command{
28+
Use: "ocm [sub-command]",
29+
Short: "The official Open Component Model (OCM) CLI",
30+
Long: `The Open Component Model command line client supports the work with OCM
31+
artifacts, like Component Archives, Common Transport Archive,
32+
Component Repositories, and Component Versions.`,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
return cmd.Help()
35+
},
36+
PersistentPreRunE: preRunE,
37+
DisableAutoGenTag: true,
38+
SilenceUsage: true,
39+
}
40+
41+
v1.RegisterConfigFlag(cmd)
42+
log.RegisterLoggingFlags(cmd.PersistentFlags())
43+
cmd.AddCommand(generate.New())
44+
cmd.AddCommand(get.New())
45+
return cmd
46+
}
47+
48+
// preRunE sets up the Cmd command with the necessary setup for all cli commands.
49+
func preRunE(cmd *cobra.Command, _ []string) error {
50+
logger, err := log.GetBaseLogger(cmd)
51+
if err != nil {
52+
return fmt.Errorf("could not retrieve logger: %w", err)
53+
}
54+
slog.SetDefault(logger)
55+
56+
setupOCMConfig(cmd)
57+
58+
if err := setupPluginManager(cmd); err != nil {
59+
return fmt.Errorf("could not setup plugin manager: %w", err)
60+
}
61+
62+
if err := setupCredentialGraph(cmd); err != nil {
63+
return fmt.Errorf("could not setup credential graph: %w", err)
64+
}
65+
66+
ocmctx.Register(cmd)
67+
68+
if parent := cmd.Parent(); parent != nil {
69+
cmd.SetOut(parent.OutOrStdout())
70+
cmd.SetErr(parent.ErrOrStderr())
71+
}
72+
73+
return nil
74+
}

cli/cmd/cmd_test.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package cmd_test
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"ocm.software/open-component-model/bindings/go/blob/filesystem"
11+
"ocm.software/open-component-model/bindings/go/ctf"
12+
descriptor "ocm.software/open-component-model/bindings/go/descriptor/runtime"
13+
"ocm.software/open-component-model/bindings/go/oci"
14+
ocictf "ocm.software/open-component-model/bindings/go/oci/ctf"
15+
ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf"
16+
"ocm.software/open-component-model/cli/cmd/internal/test"
17+
"ocm.software/open-component-model/cli/internal/reference/compref"
18+
)
19+
20+
// setupTestRepository creates a test repository with the given component versions
21+
func setupTestRepository(t *testing.T, versions ...*descriptor.Descriptor) (string, error) {
22+
r := require.New(t)
23+
archivePath := t.TempDir()
24+
fs, err := filesystem.NewFS(archivePath, os.O_RDWR)
25+
r.NoError(err, "could not create test filesystem")
26+
archive := ctf.NewFileSystemCTF(fs)
27+
helperRepo, err := oci.NewRepository(ocictf.WithCTF(ocictf.NewFromCTF(archive)))
28+
r.NoError(err, "could not create helper test repository")
29+
30+
ctx := t.Context()
31+
for _, desc := range versions {
32+
r.NoError(helperRepo.AddComponentVersion(ctx, desc), "could not add component version to test repository")
33+
}
34+
35+
return archivePath, nil
36+
}
37+
38+
// createTestDescriptor creates a test component descriptor with the given name and version
39+
func createTestDescriptor(name, version string) *descriptor.Descriptor {
40+
return &descriptor.Descriptor{
41+
Meta: descriptor.Meta{
42+
Version: "v2",
43+
},
44+
Component: descriptor.Component{
45+
ComponentMeta: descriptor.ComponentMeta{
46+
ObjectMeta: descriptor.ObjectMeta{
47+
Name: name,
48+
Version: version,
49+
},
50+
},
51+
},
52+
}
53+
}
54+
55+
// Test_Get_Component_Version_Formats tests the different output formats for the get cv command
56+
func Test_Get_Component_Version_Formats(t *testing.T) {
57+
// Setup test repository with a single component version
58+
desc := createTestDescriptor("ocm.software/test-component", "0.0.1")
59+
archivePath, err := setupTestRepository(t, desc)
60+
require.NoError(t, err)
61+
62+
ref := compref.Ref{
63+
Repository: &ctfv1.Repository{
64+
Path: archivePath,
65+
},
66+
Component: desc.Component.Name,
67+
Version: desc.Component.Version,
68+
}
69+
path := ref.String()
70+
71+
tests := []struct {
72+
name string
73+
args []string
74+
expectedOutput string
75+
expectedError bool
76+
}{
77+
{
78+
name: "Default Options (Table)",
79+
args: []string{"get", "cv", path},
80+
expectedOutput: `
81+
COMPONENT │ VERSION │ PROVIDER
82+
─────────────────────────────┼─────────┼──────────
83+
ocm.software/test-component │ 0.0.1 │
84+
`,
85+
expectedError: false,
86+
},
87+
{
88+
name: "YAML output",
89+
args: []string{"get", "cv", path, "--output=yaml"},
90+
expectedOutput: `
91+
component:
92+
componentReferences: null
93+
name: ocm.software/test-component
94+
provider: ""
95+
repositoryContexts: null
96+
resources: null
97+
sources: null
98+
version: 0.0.1
99+
meta:
100+
schemaVersion: v2
101+
`,
102+
expectedError: false,
103+
},
104+
{
105+
name: "JSON output",
106+
args: []string{"get", "cv", path, "--output=json"},
107+
expectedOutput: "", // JSON output is handled differently
108+
expectedError: false,
109+
},
110+
{
111+
name: "Invalid output format",
112+
args: []string{"get", "cv", path, "--output=invalid"},
113+
expectedOutput: "",
114+
expectedError: true,
115+
},
116+
{
117+
name: "Non-existent component",
118+
args: []string{"get", "cv", "non-existent"},
119+
expectedOutput: "",
120+
expectedError: true,
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
r := require.New(t)
127+
logs := test.NewJSONLogReader()
128+
_, err = test.OCM(t, test.WithArgs(tt.args...), test.WithOutput(logs))
129+
130+
if tt.expectedError {
131+
r.Error(err, "expected error but got none")
132+
return
133+
}
134+
135+
r.NoError(err, "failed to run command")
136+
entries, err := logs.List()
137+
r.NoError(err, "failed to list log entries")
138+
139+
if tt.args[len(tt.args)-1] == "--output=json" {
140+
// Handle JSON output separately
141+
r.EqualValues(map[string]any{
142+
"component": map[string]any{
143+
"componentReferences": nil,
144+
"name": "ocm.software/test-component",
145+
"provider": "",
146+
"repositoryContexts": nil,
147+
"resources": nil,
148+
"sources": nil,
149+
"version": "0.0.1",
150+
},
151+
"meta": map[string]any{
152+
"schemaVersion": "v2",
153+
},
154+
}, entries[len(entries)-1].Extras)
155+
} else {
156+
discarded := logs.GetDiscarded()
157+
r.NotEmpty(discarded, "expected non json logs to contain output")
158+
r.EqualValues(strings.TrimSpace(tt.expectedOutput), strings.TrimSpace(discarded), "expected output")
159+
}
160+
})
161+
}
162+
}
163+
164+
// Test_List_Component_Version_Variations tests different variations of listing component versions
165+
func Test_List_Component_Version_Variations(t *testing.T) {
166+
// Setup test repository with multiple component versions
167+
desc1 := createTestDescriptor("ocm.software/test-component", "0.0.1")
168+
desc2 := createTestDescriptor("ocm.software/test-component", "0.0.2")
169+
archivePath, err := setupTestRepository(t, desc1, desc2)
170+
require.NoError(t, err)
171+
172+
ref := compref.Ref{
173+
Repository: &ctfv1.Repository{
174+
Path: archivePath,
175+
},
176+
Component: desc1.Component.Name,
177+
}
178+
179+
path := ref.String()
180+
181+
tests := []struct {
182+
name string
183+
args []string
184+
expectedOutput string
185+
expectedError bool
186+
}{
187+
{
188+
name: "Default Options (Table) - all versions",
189+
args: []string{"get", "cv", path},
190+
expectedOutput: `
191+
COMPONENT │ VERSION │ PROVIDER
192+
─────────────────────────────┼─────────┼──────────
193+
ocm.software/test-component │ 0.0.2 │
194+
│ 0.0.1 │
195+
`,
196+
expectedError: false,
197+
},
198+
{
199+
name: "Latest version only",
200+
args: []string{"get", "cv", path, "--latest"},
201+
expectedOutput: `
202+
COMPONENT │ VERSION │ PROVIDER
203+
─────────────────────────────┼─────────┼──────────
204+
ocm.software/test-component │ 0.0.2 │
205+
`,
206+
expectedError: false,
207+
},
208+
{
209+
name: "Semver constraint",
210+
args: []string{"get", "cv", path, "--semver-constraint", "< 0.0.2"},
211+
expectedOutput: `
212+
COMPONENT │ VERSION │ PROVIDER
213+
─────────────────────────────┼─────────┼──────────
214+
ocm.software/test-component │ 0.0.1 │
215+
`,
216+
expectedError: false,
217+
},
218+
{
219+
name: "Invalid semver constraint",
220+
args: []string{"get", "cv", path, "--semver-constraint", "invalid"},
221+
expectedOutput: "",
222+
expectedError: true,
223+
},
224+
{
225+
name: "Non-existent component",
226+
args: []string{"get", "cv", "non-existent"},
227+
expectedOutput: "",
228+
expectedError: true,
229+
},
230+
}
231+
232+
for _, tt := range tests {
233+
t.Run(tt.name, func(t *testing.T) {
234+
r := require.New(t)
235+
logs := test.NewJSONLogReader()
236+
_, err = test.OCM(t, test.WithArgs(tt.args...), test.WithOutput(logs))
237+
238+
if tt.expectedError {
239+
r.Error(err, "expected error but got none")
240+
return
241+
}
242+
243+
r.NoError(err, "failed to run command")
244+
_, err := logs.List()
245+
r.NoError(err, "failed to list log entries")
246+
247+
discarded := logs.GetDiscarded()
248+
r.NotEmpty(discarded, "expected non json logs to contain table")
249+
r.EqualValues(strings.TrimSpace(tt.expectedOutput), strings.TrimSpace(discarded), "expected table output")
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)