Skip to content

Commit 2fb06d2

Browse files
authored
feat: add ocmTransfer command (#8)
* implement ocm transfer * add documentation * add tests
1 parent 7238e01 commit 2fb06d2

File tree

10 files changed

+382
-7
lines changed

10 files changed

+382
-7
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
[![REUSE status](https://api.reuse.software/badge/github.com/openmcp-project/bootstrapper)](https://api.reuse.software/info/github.com/openmcp-project/bootstrapper)
22

3-
# bootstrapper
3+
# openmcp bootstrapper
44

55
## About this project
66

77
The openmcp bootstrapper is a command line tool that is able to set up an openmcp landscape initially and to update existing openmcp landscapes with new versions of the openmcp project.
88

9+
Supported commands:
10+
* `ocmTransfer`: Transfers the specified OCM component version from the source location to the destination location.
11+
12+
### `ocmTransfer`
13+
14+
The `ocmTransfer` command is used to transfer an OCM component version from a source location to a destination location.
15+
The `ocmTransfer` requires the following parameters:
16+
* `source`: The source location of the OCM component version to be transferred.
17+
* `destination`: The destination location where the OCM component version should be transferred to.
18+
19+
Optional parameters:
20+
* `--config`: Path to the OCM configuration file.
21+
22+
```shell
23+
bootstrapper ocmTransfer --source <source-location> --destination <destination-location> --config <path-to-ocm-config>
24+
```
25+
26+
This command internally calls the OCM cli with the following command and arguments:
27+
28+
```shell
29+
ocm transfer componentversion --recursive --copy-resources --copy-sources <source-location> <destination-location> --config <path-to-ocm-config>
30+
```
31+
32+
Example:
33+
```shell
34+
ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ./ctf
35+
ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ghcr.io/my-github-user
36+
```
37+
38+
39+
940
## Requirements and Setup
1041

1142
This project uses the [cobra library](https://github.com/spf13/cobra) for command line parsing.

cmd/ocmTransfer.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package cmd
2+
3+
import (
4+
ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// ocmTransferCmd represents the "ocm transfer componentversion" command
10+
var ocmTransferCmd = &cobra.Command{
11+
Use: "ocmTransfer source destination",
12+
Short: "Transfer an OCM component from a source to a destination",
13+
Long: `Transfers the specified OCM component version from the source location to the destination location.`,
14+
Aliases: []string{
15+
"transfer",
16+
},
17+
Args: cobra.ExactArgs(2),
18+
ArgAliases: []string{
19+
"source",
20+
"destination",
21+
},
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
transferCommands := []string{
24+
"transfer",
25+
"componentversion",
26+
}
27+
28+
transferArgs := []string{
29+
"--recursive",
30+
"--copy-resources",
31+
"--copy-sources",
32+
args[0], // source
33+
args[1], // destination
34+
}
35+
36+
return ocmcli.Execute(cmd.Context(), transferCommands, transferArgs, cmd.Flag("config").Value.String())
37+
},
38+
}
39+
40+
func init() {
41+
RootCmd.AddCommand(ocmTransferCmd)
42+
43+
ocmTransferCmd.PersistentFlags().StringP("config", "c", "", "ocm configuration file")
44+
}

cmd/ocmTransfer_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cmd_test
2+
3+
import (
4+
"errors"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/openmcp-project/bootstrapper/cmd"
11+
testutil "github.com/openmcp-project/bootstrapper/test/utils"
12+
)
13+
14+
func TestOcmTransfer(t *testing.T) {
15+
expectError := errors.New("expected error")
16+
17+
testutil.DownloadOCMAndAddToPath(t)
18+
19+
ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t)
20+
ctfOut := filepath.Join(t.TempDir(), "ctfOut")
21+
22+
testCases := []struct {
23+
desc string
24+
arguments []string
25+
expectedError error
26+
}{
27+
{
28+
desc: "No arguments specified",
29+
arguments: []string{},
30+
expectedError: expectError,
31+
},
32+
{
33+
desc: "One argument specified",
34+
arguments: []string{"source"},
35+
expectedError: expectError,
36+
},
37+
{
38+
desc: "Two arguments specified",
39+
arguments: []string{ctfIn, ctfOut},
40+
expectedError: nil,
41+
},
42+
}
43+
44+
for _, tc := range testCases {
45+
t.Run(tc.desc, func(t *testing.T) {
46+
root := cmd.RootCmd
47+
args := []string{"ocmTransfer"}
48+
if len(tc.arguments) > 0 {
49+
args = append(args, tc.arguments...)
50+
}
51+
root.SetArgs(args)
52+
53+
err := root.Execute()
54+
if tc.expectedError != nil {
55+
assert.Error(t, err)
56+
} else {
57+
assert.NoError(t, err)
58+
}
59+
})
60+
}
61+
}

cmd/root.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
// rootCmd represents the base command when called without any subcommands
10-
var rootCmd = &cobra.Command{
10+
var RootCmd = &cobra.Command{
1111
Use: "openmcp-bootstrapper",
1212
Short: "The openMCP bootstrapper CLI",
1313
Long: `The openMCP bootstrapper CLI is a command-line interface
@@ -20,7 +20,7 @@ for bootstrapping and updating openMCP landscapes.`,
2020
// Execute adds all child commands to the root command and sets flags appropriately.
2121
// This is called by main.main(). It only needs to happen once to the rootCmd.
2222
func Execute() {
23-
err := rootCmd.Execute()
23+
err := RootCmd.Execute()
2424
if err != nil {
2525
os.Exit(1)
2626
}
@@ -35,5 +35,5 @@ func init() {
3535

3636
// Cobra also supports local flags, which will only run
3737
// when this action is called directly.
38-
// rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
38+
// RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
3939
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
components:
2+
- name: github.com/openmcp-project/bootstrapper/test
3+
version: v0.0.1
4+
provider:
5+
name: openmcp-project

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ module github.com/openmcp-project/bootstrapper
22

33
go 1.24.5
44

5-
require github.com/spf13/cobra v1.9.1
5+
require (
6+
github.com/spf13/cobra v1.9.1
7+
github.com/stretchr/testify v1.10.0
8+
gopkg.in/yaml.v3 v3.0.1
9+
)
610

711
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
813
github.com/inconshreveable/mousetrap v1.1.0 // indirect
14+
github.com/pmezard/go-difflib v1.0.0 // indirect
915
github.com/spf13/pflag v1.0.7 // indirect
1016
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
35
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
48
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
59
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
610
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
711
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
812
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
913
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
14+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
15+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
16+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1017
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
18+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1119
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/ocm-cli/ocm.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package ocm_cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"gopkg.in/yaml.v3"
7+
"os"
8+
"os/exec"
9+
)
10+
11+
const (
12+
// NoOcmConfig is a constant to indicate that no OCM configuration file is being provided.
13+
NoOcmConfig = ""
14+
)
15+
16+
// Execute runs the specified OCM command with the provided arguments and configuration.
17+
// It captures the command's output and errors, and returns an error if the command fails.
18+
// The `commands` parameter is a slice of strings representing the OCM command and its subcommands.
19+
// The `args` parameter is a slice of strings representing the arguments to the command.
20+
// The `ocmConfig` parameter is a string representing the path to the OCM configuration file. Passing `NoOcmConfig` indicates that no configuration file should be used.
21+
func Execute(ctx context.Context, commands []string, args []string, ocmConfig string) error {
22+
var flags []string
23+
24+
flags = append(flags, commands...)
25+
flags = append(flags, args...)
26+
27+
if ocmConfig != NoOcmConfig {
28+
flags = append(flags, "--config", ocmConfig)
29+
}
30+
31+
cmd := exec.CommandContext(ctx, "ocm", flags...)
32+
cmd.Stdout = os.Stdout
33+
cmd.Stderr = os.Stderr
34+
35+
if err := cmd.Start(); err != nil {
36+
return fmt.Errorf("error starting ocm command: %w", err)
37+
}
38+
39+
if err := cmd.Wait(); err != nil {
40+
return fmt.Errorf("error waiting for ocm command to finish: %w", err)
41+
}
42+
43+
return nil
44+
}
45+
46+
// ComponentVersion represents a version of an OCM component.
47+
type ComponentVersion struct {
48+
// Component is the OCM component associated with this version.
49+
Component Component `json:"component"`
50+
}
51+
52+
// Component represents an OCM component with its name, version, references to other components, and resources.
53+
type Component struct {
54+
// Name is the name of the component.
55+
Name string `yaml:"name"`
56+
// Version is the version of the component.
57+
Version string `yaml:"version"`
58+
// ComponentReferences is a list of references to other components that this component depends on.
59+
ComponentReferences []ComponentReference `yaml:"componentReferences"`
60+
// Resources is a list of resources associated with this component, including their names, versions, types, and access information.
61+
Resources []Resource `yaml:"resources"`
62+
}
63+
64+
// ComponentReference represents a reference to another component, including its name, version, and the name of the component it refers to.
65+
type ComponentReference struct {
66+
// Name is the name of the component reference.
67+
Name string `yaml:"name"`
68+
// Version is the version of the component reference.
69+
Version string `yaml:"version"`
70+
// ComponentName is the name of the component that this reference points to.
71+
ComponentName string `yaml:"componentName"`
72+
}
73+
74+
// Resource represents a resource associated with a component, including its name, version, type, and access information.
75+
type Resource struct {
76+
// Name is the name of the resource.
77+
Name string `yaml:"name"`
78+
// Version is the version of the resource.
79+
Version string `yaml:"version"`
80+
// Type is the content type of the resource.
81+
Type string `yaml:"type"`
82+
// Access contains the information on how to access the resource.
83+
Access Access `yaml:"access"`
84+
}
85+
86+
// Access represents the access information for a resource, including the type of access.
87+
type Access struct {
88+
// Type is the content type of access to the resource.
89+
Type string `yaml:"type"`
90+
// ImageReference is the reference to the image if the Type is "ociArtifact".
91+
ImageReference string `yaml:"imageReference"`
92+
}
93+
94+
// GetResource retrieves a resource by its name from the component version.
95+
func (cv *ComponentVersion) GetResource(name string) (*Resource, error) {
96+
for _, resource := range cv.Component.Resources {
97+
if resource.Name == name {
98+
return &resource, nil
99+
}
100+
}
101+
return nil, fmt.Errorf("resource %s not found in component version %s", name, cv.Component.Name)
102+
}
103+
104+
// GetComponentReference retrieves a component reference by its name from the component version.
105+
func (cv *ComponentVersion) GetComponentReference(name string) (*ComponentReference, error) {
106+
for _, ref := range cv.Component.ComponentReferences {
107+
if ref.Name == name {
108+
return &ref, nil
109+
}
110+
}
111+
return nil, fmt.Errorf("component reference %s not found in component version %s", name, cv.Component.Name)
112+
}
113+
114+
// GetComponentVersion retrieves a component version by its reference using the OCM CLI.
115+
func GetComponentVersion(ctx context.Context, componentReference string, ocmConfig string) (*ComponentVersion, error) {
116+
flags := []string{
117+
"get",
118+
"componentversion",
119+
"--output", "yaml",
120+
componentReference,
121+
}
122+
123+
if ocmConfig != NoOcmConfig {
124+
flags = append(flags, "--config", ocmConfig)
125+
}
126+
127+
cmd := exec.CommandContext(ctx, "ocm", flags...)
128+
out, err := cmd.CombinedOutput()
129+
if err != nil {
130+
return nil, fmt.Errorf("error executing ocm command: %w", err)
131+
}
132+
133+
var cv ComponentVersion
134+
err = yaml.Unmarshal(out, &cv)
135+
if err != nil {
136+
return nil, fmt.Errorf("error unmarshalling component version: %w", err)
137+
}
138+
139+
return &cv, nil
140+
}

renovate.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@
3939
],
4040
"customManagers": [
4141
{
42-
"description": "All component dependencies and their versions used in the Dockerfile.",
42+
"description": "All component dependencies in other locations.",
4343
"customType": "regex",
4444
"managerFilePatterns": [
45-
"/Dockerfile/"
45+
"/Dockerfile/",
46+
"/test/util/ocm.go"
4647
],
4748
"matchStrings": [
4849
"# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s.+?(_version|_VERSION)=\"?(?<currentValue>.+?)\"?\\s"

0 commit comments

Comments
 (0)