diff --git a/README.md b/README.md index b7f2630..f0cf82e 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,32 @@ 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. Supported commands: -* `ocmTransfer`: Transfers the specified OCM component version from the source location to the destination location. +* `ocmTransfer`: Transfers the specified OCM component version from the source location to the target location. ### `ocmTransfer` -The `ocmTransfer` command is used to transfer an OCM component version from a source location to a destination location. +The `ocmTransfer` command is used to transfer an OCM component version from a source location to a target location. The `ocmTransfer` requires the following parameters: * `source`: The source location of the OCM component version to be transferred. -* `destination`: The destination location where the OCM component version should be transferred to. +* `target`: The target location where the OCM component version should be transferred to. Optional parameters: * `--config`: Path to the OCM configuration file. ```shell -bootstrapper ocmTransfer --source --destination --config +openmcp-bootstrapper ocmTransfer --config ``` This command internally calls the OCM cli with the following command and arguments: ```shell -ocm transfer componentversion --recursive --copy-resources --copy-sources --config +ocm --config transfer componentversion --recursive --copy-resources --copy-sources ``` Example: ```shell -ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ./ctf -ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ghcr.io/my-github-user +openmcp-bootstrapper ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ./ctf +openmcp-bootstrapper ocmTransfer ghcr.io/openmcp-project/components//github.com/openmcp-project/openmcp:v0.0.11 ghcr.io/my-github-user ``` diff --git a/Taskfile.yaml b/Taskfile.yaml index 042dcad..2248fe6 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -9,7 +9,7 @@ includes: NESTED_MODULES: '' API_DIRS: '' MANIFEST_OUT: '' - CODE_DIRS: '{{.ROOT_DIR}}/cmd/...' + CODE_DIRS: '{{.ROOT_DIR}}/cmd/... {{.ROOT_DIR}}/internal/... {{.ROOT_DIR}}/test/...' COMPONENTS: 'openmcp-bootstrapper' REPO_URL: 'https://github.com/openmcp-project/bootstrapper' GENERATE_DOCS_INDEX: "true" diff --git a/cmd/ocmTransfer.go b/cmd/ocmTransfer.go index d102cb4..f5e157a 100644 --- a/cmd/ocmTransfer.go +++ b/cmd/ocmTransfer.go @@ -8,16 +8,16 @@ import ( // ocmTransferCmd represents the "ocm transfer componentversion" command var ocmTransferCmd = &cobra.Command{ - Use: "ocmTransfer source destination", - Short: "Transfer an OCM component from a source to a destination", - Long: `Transfers the specified OCM component version from the source location to the destination location.`, + Use: "ocmTransfer source target", + Short: "Transfer an OCM component from a source to a target location", + Long: `Transfers the specified OCM component version from the source location to the target location.`, Aliases: []string{ "transfer", }, Args: cobra.ExactArgs(2), ArgAliases: []string{ "source", - "destination", + "target", }, RunE: func(cmd *cobra.Command, args []string) error { transferCommands := []string{ @@ -30,7 +30,7 @@ var ocmTransferCmd = &cobra.Command{ "--copy-resources", "--copy-sources", args[0], // source - args[1], // destination + args[1], // target } return ocmcli.Execute(cmd.Context(), transferCommands, transferArgs, cmd.Flag("config").Value.String()) diff --git a/go.mod b/go.mod index 5d707b7..abe0b45 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.24.5 require ( github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 - gopkg.in/yaml.v3 v3.0.1 + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -13,4 +14,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.7 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 45e8142..349e1a3 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -13,7 +15,15 @@ github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/ocm-cli/ocm.go b/internal/ocm-cli/ocm.go index 61c6887..b2fd4ad 100644 --- a/internal/ocm-cli/ocm.go +++ b/internal/ocm-cli/ocm.go @@ -3,9 +3,10 @@ package ocm_cli import ( "context" "fmt" - "gopkg.in/yaml.v3" "os" "os/exec" + + "sigs.k8s.io/yaml" ) const ( @@ -19,16 +20,16 @@ const ( // The `args` parameter is a slice of strings representing the arguments to the command. // The `ocmConfig` parameter is a string representing the path to the OCM configuration file. Passing `NoOcmConfig` indicates that no configuration file should be used. func Execute(ctx context.Context, commands []string, args []string, ocmConfig string) error { - var flags []string - - flags = append(flags, commands...) - flags = append(flags, args...) + var ocmArgs []string if ocmConfig != NoOcmConfig { - flags = append(flags, "--config", ocmConfig) + ocmArgs = append(ocmArgs, "--config", ocmConfig) } - cmd := exec.CommandContext(ctx, "ocm", flags...) + ocmArgs = append(ocmArgs, commands...) + ocmArgs = append(ocmArgs, args...) + + cmd := exec.CommandContext(ctx, "ocm", ocmArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -52,43 +53,47 @@ type ComponentVersion struct { // Component represents an OCM component with its name, version, references to other components, and resources. type Component struct { // Name is the name of the component. - Name string `yaml:"name"` + Name string `json:"name"` // Version is the version of the component. - Version string `yaml:"version"` + Version string `json:"version"` // ComponentReferences is a list of references to other components that this component depends on. ComponentReferences []ComponentReference `yaml:"componentReferences"` // Resources is a list of resources associated with this component, including their names, versions, types, and access information. - Resources []Resource `yaml:"resources"` + Resources []Resource `json:"resources"` } // ComponentReference represents a reference to another component, including its name, version, and the name of the component it refers to. type ComponentReference struct { // Name is the name of the component reference. - Name string `yaml:"name"` + Name string `json:"name"` // Version is the version of the component reference. - Version string `yaml:"version"` + Version string `json:"version"` // ComponentName is the name of the component that this reference points to. - ComponentName string `yaml:"componentName"` + ComponentName string `json:"componentName"` } // Resource represents a resource associated with a component, including its name, version, type, and access information. type Resource struct { // Name is the name of the resource. - Name string `yaml:"name"` + Name string `json:"name"` // Version is the version of the resource. - Version string `yaml:"version"` + Version string `json:"version"` // Type is the content type of the resource. - Type string `yaml:"type"` + Type string `json:"type"` // Access contains the information on how to access the resource. - Access Access `yaml:"access"` + Access Access `json:"access"` } // Access represents the access information for a resource, including the type of access. type Access struct { - // Type is the content type of access to the resource. - Type string `yaml:"type"` + // Type specifies the access type of the resource. + Type string `json:"type"` // ImageReference is the reference to the image if the Type is "ociArtifact". - ImageReference string `yaml:"imageReference"` + ImageReference *string `json:"imageReference"` + // LocalReference specifies a component local access + LocalReference *string `json:"localReference"` + // MediaType is the media type of the resource + MediaType *string `json:"mediaType"` } // GetResource retrieves a resource by its name from the component version. @@ -113,21 +118,18 @@ func (cv *ComponentVersion) GetComponentReference(name string) (*ComponentRefere // GetComponentVersion retrieves a component version by its reference using the OCM CLI. func GetComponentVersion(ctx context.Context, componentReference string, ocmConfig string) (*ComponentVersion, error) { - flags := []string{ - "get", - "componentversion", - "--output", "yaml", - componentReference, - } + var ocmArgs []string if ocmConfig != NoOcmConfig { - flags = append(flags, "--config", ocmConfig) + ocmArgs = append(ocmArgs, "--config", ocmConfig) } - cmd := exec.CommandContext(ctx, "ocm", flags...) + ocmArgs = append(ocmArgs, "get", "componentversion", "--output", "yaml", componentReference) + + cmd := exec.CommandContext(ctx, "ocm", ocmArgs...) out, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("error executing ocm command: %w", err) + return nil, fmt.Errorf("error executing ocm command: %w, %q", err, out) } var cv ComponentVersion diff --git a/internal/ocm-cli/ocm_test.go b/internal/ocm-cli/ocm_test.go new file mode 100644 index 0000000..d6c4298 --- /dev/null +++ b/internal/ocm-cli/ocm_test.go @@ -0,0 +1,140 @@ +package ocm_cli_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" + + ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" + testutil "github.com/openmcp-project/bootstrapper/test/utils" +) + +func TestExecute(t *testing.T) { + expectError := errors.New("expected error") + + testutil.DownloadOCMAndAddToPath(t) + + ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t) + + testCases := []struct { + desc string + commands []string + arguments []string + ocmConfig string + expectedError error + }{ + { + desc: "get componentversion", + commands: []string{"get", "componentversion"}, + arguments: []string{"--output", "yaml", ctfIn}, + ocmConfig: ocmcli.NoOcmConfig, + expectedError: nil, + }, + { + desc: "get componentversion with invalid argument", + commands: []string{"get", "componentversion"}, + arguments: []string{"--output", "yaml", "invalid-argument"}, + ocmConfig: ocmcli.NoOcmConfig, + expectedError: expectError, + }, + { + desc: "get componentversion with ocm config", + commands: []string{"get", "componentversion"}, + arguments: []string{"--output", "yaml", ctfIn}, + ocmConfig: "./testdata/ocm-config.yaml", + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + err := ocmcli.Execute(t.Context(), tc.commands, tc.arguments, tc.ocmConfig) + + if tc.expectedError != nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetComponentVersion(t *testing.T) { + expectError := errors.New("expected error") + testutil.DownloadOCMAndAddToPath(t) + + ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t) + + testCases := []struct { + desc string + componentRef string + ocmConfig string + expectedError error + verify func(cv *ocmcli.ComponentVersion) + }{ + { + desc: "get component version", + componentRef: ctfIn, + ocmConfig: ocmcli.NoOcmConfig, + expectedError: nil, + verify: func(cv *ocmcli.ComponentVersion) { + assert.Equal(t, cv.Component.Name, "github.com/openmcp-project/bootstrapper/test") + assert.Equal(t, cv.Component.Version, "v0.0.1") + assert.Len(t, cv.Component.ComponentReferences, 2) + assert.Len(t, cv.Component.Resources, 1) + + assert.Contains(t, cv.Component.ComponentReferences, ocmcli.ComponentReference{ + Name: "bootstrapper-dependency-a", + Version: "v0.2.0", + ComponentName: "github.com/openmcp-project/bootstrapper-dependency-a", + }) + assert.Contains(t, cv.Component.ComponentReferences, ocmcli.ComponentReference{ + Name: "bootstrapper-dependency-b", + Version: "v0.3.0", + ComponentName: "github.com/openmcp-project/bootstrapper-dependency-b", + }) + + assert.Contains(t, cv.Component.Resources, ocmcli.Resource{ + Name: "test-resource", + Version: "v0.0.1", + Type: "blob", + Access: ocmcli.Access{ + Type: "localBlob", + LocalReference: cv.Component.Resources[0].Access.LocalReference, + MediaType: ptr.To("application/octet-stream"), + }, + }) + }, + }, + { + desc: "get component version with ocm config", + componentRef: ctfIn, + ocmConfig: "./testdata/ocm-config.yaml", + expectedError: nil, + }, + { + desc: "get component version with invalid reference", + componentRef: "invalid-component-ref", + ocmConfig: ocmcli.NoOcmConfig, + expectedError: expectError, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + cv, err := ocmcli.GetComponentVersion(t.Context(), tc.componentRef, tc.ocmConfig) + + if tc.expectedError != nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if tc.verify != nil { + tc.verify(cv) + } + }) + } +} diff --git a/internal/ocm-cli/testdata/component-constructor.yaml b/internal/ocm-cli/testdata/component-constructor.yaml new file mode 100644 index 0000000..7cfb144 --- /dev/null +++ b/internal/ocm-cli/testdata/component-constructor.yaml @@ -0,0 +1,21 @@ +components: + - name: github.com/openmcp-project/bootstrapper/test + version: v0.0.1 + provider: + name: openmcp-project + + componentReferences: + - componentName: github.com/openmcp-project/bootstrapper-dependency-a + name: bootstrapper-dependency-a + version: v0.2.0 + + - componentName: github.com/openmcp-project/bootstrapper-dependency-b + name: bootstrapper-dependency-b + version: v0.3.0 + + resources: + - name: test-resource + type: blob + input: + type: file + path: ./test-resource.yaml diff --git a/internal/ocm-cli/testdata/ocm-config.yaml b/internal/ocm-cli/testdata/ocm-config.yaml new file mode 100644 index 0000000..9ee648d --- /dev/null +++ b/internal/ocm-cli/testdata/ocm-config.yaml @@ -0,0 +1,7 @@ +type: generic.config.ocm.software/v1 +configurations: + - type: credentials.config.ocm.software + repositories: + - repository: + type: DockerConfig/v1 + dockerConfigFile: "~/.docker/config.json" \ No newline at end of file diff --git a/internal/ocm-cli/testdata/test-resource.yaml b/internal/ocm-cli/testdata/test-resource.yaml new file mode 100644 index 0000000..2ea1df8 --- /dev/null +++ b/internal/ocm-cli/testdata/test-resource.yaml @@ -0,0 +1,4 @@ +config: + vars: + - a: "a" + - b: "b" diff --git a/test/utils/ocm.go b/test/utils/ocm.go index 9637fe6..4926d3a 100644 --- a/test/utils/ocm.go +++ b/test/utils/ocm.go @@ -15,45 +15,98 @@ const ( OCM_VERSION = "0.27.0" ) +var ( + CacheDirRoot = filepath.Join(os.TempDir(), "openmcp-bootstrapper-test") +) + // DownloadOCMAndAddToPath downloads the OCM cli for the current platform and puts it to the PATH of the test func DownloadOCMAndAddToPath(t *testing.T) { t.Helper() - downloadURL := "https://github.com/open-component-model/ocm/releases/download/v" + - OCM_VERSION + "/ocm-" + OCM_VERSION + "-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" - - tempDir := t.TempDir() - archivePath := filepath.Join(tempDir, "ocm.tar.gz") - out, err := os.Create(archivePath) - if err != nil { - t.Fatalf("failed to create file: %v", err) + cacheDir := filepath.Join(CacheDirRoot, "ocm-cli-cache") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + t.Fatalf("failed to create cache dir: %v", err) } - defer out.Close() - resp, err := http.Get(downloadURL) - if err != nil { - t.Fatalf("failed to download ocm: %v", err) - } - defer resp.Body.Close() + ocmBinaryName := "ocm-" + OCM_VERSION + "-" + runtime.GOOS + "-" + runtime.GOARCH + ocmPath := filepath.Join(cacheDir, ocmBinaryName) - _, err = io.Copy(out, resp.Body) - if err != nil { - t.Fatalf("failed to save ocm: %v", err) - } + if _, err := os.Stat(ocmPath); os.IsNotExist(err) { + t.Log("Downloading OCM as it is not present in the cache directory, starting download...") - // Extract the tar.gz - cmd := exec.Command("tar", "-xzf", archivePath, "-C", tempDir) - if err := cmd.Run(); err != nil { - t.Fatalf("failed to extract ocm: %v", err) - } + downloadURL := "https://github.com/open-component-model/ocm/releases/download/v" + + OCM_VERSION + "/ocm-" + OCM_VERSION + "-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" + + tempDir := t.TempDir() + archivePath := filepath.Join(tempDir, "ocm.tar.gz") + out, err := os.Create(archivePath) + if err != nil { + t.Fatalf("failed to create file: %v", err) + } + defer func(out *os.File) { + err := out.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + }(out) + + resp, err := http.Get(downloadURL) + if err != nil { + t.Fatalf("failed to download ocm: %v", err) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + t.Fatalf("failed to close response body: %v", err) + } + }(resp.Body) + + _, err = io.Copy(out, resp.Body) + if err != nil { + t.Fatalf("failed to save ocm: %v", err) + } + + // Extract the tar.gz + cmd := exec.Command("tar", "-xzf", archivePath, "-C", tempDir) + if err := cmd.Run(); err != nil { + t.Fatalf("failed to extract ocm: %v", err) + } - // Find the ocm binary - ocmPath := filepath.Join(tempDir, "ocm") - if _, err := os.Stat(ocmPath); err != nil { - t.Fatalf("ocm binary not found after extraction: %v", err) + // Move the ocm binary to the cache dir + binPath := filepath.Join(tempDir, "ocm") + if _, err := os.Stat(binPath); err != nil { + t.Fatalf("ocm binary not found after extraction: %v", err) + } + if err := os.Rename(binPath, ocmPath); err != nil { + t.Fatalf("failed to move ocm binary to cache: %v", err) + } + if err := os.Chmod(ocmPath, 0o755); err != nil { + t.Fatalf("failed to chmod ocm binary: %v", err) + } + + // if symlink already exists, remove it + symlinkPath := filepath.Join(cacheDir, "ocm") + if _, err := os.Lstat(symlinkPath); err == nil { + if err := os.Remove(symlinkPath); err != nil { + t.Fatalf("failed to remove existing symlink: %v", err) + } + } else if !os.IsNotExist(err) { + t.Fatalf("failed to check existing symlink: %v", err) + } + + // create symlink to the ocm binary + if err := os.Symlink(ocmPath, symlinkPath); err != nil { + t.Fatalf("failed to create symlink for ocm binary: %v", err) + } + } else { + t.Log("OCM binary already exists in the cache directory, skipping download.") } - t.Setenv("PATH", tempDir+":"+os.Getenv("PATH")) + // Prepend the cache dir to PATH + err := os.Setenv("PATH", cacheDir+":"+os.Getenv("PATH")) + if err != nil { + t.Fatalf("failed to set PATH environment variable: %v", err) + } } // BuildComponent builds the component for the specified componentConstructorLocation and returns the ctf out directory.