From ab3ae4f6597ab40af0cc482857344f4194ad9a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 30 Jul 2025 16:22:55 +0200 Subject: [PATCH 1/3] implement ocm transfer --- cmd/ocmTransfer.go | 44 +++++++++++++ go.mod | 5 +- go.sum | 1 + internal/ocm-cli/ocm.go | 140 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 cmd/ocmTransfer.go create mode 100644 internal/ocm-cli/ocm.go diff --git a/cmd/ocmTransfer.go b/cmd/ocmTransfer.go new file mode 100644 index 0000000..f313b58 --- /dev/null +++ b/cmd/ocmTransfer.go @@ -0,0 +1,44 @@ +package cmd + +import ( + ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" + + "github.com/spf13/cobra" +) + +// 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.`, + Aliases: []string{ + "transfer", + }, + Args: cobra.ExactArgs(2), + ArgAliases: []string{ + "source", + "destination", + }, + RunE: func(cmd *cobra.Command, args []string) error { + transferCommands := []string{ + "transfer", + "componentversion", + } + + transferArgs := []string{ + "--recursive", + "--copy-resources", + "--copy-sources", + args[0], // source + args[1], // destination + } + + return ocmcli.Execute(cmd.Context(), transferCommands, transferArgs, cmd.Flag("config").Value.String()) + }, +} + +func init() { + rootCmd.AddCommand(ocmTransferCmd) + + ocmTransferCmd.PersistentFlags().StringP("config", "c", "", "ocm configuration file") +} diff --git a/go.mod b/go.mod index 2946cb0..ea48fc5 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/openmcp-project/bootstrapper go 1.24.5 -require github.com/spf13/cobra v1.9.1 +require ( + github.com/spf13/cobra v1.9.1 + gopkg.in/yaml.v3 v3.0.1 +) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4aae07f..8a79d0f 100644 --- a/go.sum +++ b/go.sum @@ -8,4 +8,5 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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= diff --git a/internal/ocm-cli/ocm.go b/internal/ocm-cli/ocm.go new file mode 100644 index 0000000..61c6887 --- /dev/null +++ b/internal/ocm-cli/ocm.go @@ -0,0 +1,140 @@ +package ocm_cli + +import ( + "context" + "fmt" + "gopkg.in/yaml.v3" + "os" + "os/exec" +) + +const ( + // NoOcmConfig is a constant to indicate that no OCM configuration file is being provided. + NoOcmConfig = "" +) + +// Execute runs the specified OCM command with the provided arguments and configuration. +// It captures the command's output and errors, and returns an error if the command fails. +// The `commands` parameter is a slice of strings representing the OCM command and its subcommands. +// 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...) + + if ocmConfig != NoOcmConfig { + flags = append(flags, "--config", ocmConfig) + } + + cmd := exec.CommandContext(ctx, "ocm", flags...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("error starting ocm command: %w", err) + } + + if err := cmd.Wait(); err != nil { + return fmt.Errorf("error waiting for ocm command to finish: %w", err) + } + + return nil +} + +// ComponentVersion represents a version of an OCM component. +type ComponentVersion struct { + // Component is the OCM component associated with this version. + Component Component `json:"component"` +} + +// 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"` + // Version is the version of the component. + Version string `yaml:"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"` +} + +// 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"` + // Version is the version of the component reference. + Version string `yaml:"version"` + // ComponentName is the name of the component that this reference points to. + ComponentName string `yaml:"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"` + // Version is the version of the resource. + Version string `yaml:"version"` + // Type is the content type of the resource. + Type string `yaml:"type"` + // Access contains the information on how to access the resource. + Access Access `yaml:"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"` + // ImageReference is the reference to the image if the Type is "ociArtifact". + ImageReference string `yaml:"imageReference"` +} + +// GetResource retrieves a resource by its name from the component version. +func (cv *ComponentVersion) GetResource(name string) (*Resource, error) { + for _, resource := range cv.Component.Resources { + if resource.Name == name { + return &resource, nil + } + } + return nil, fmt.Errorf("resource %s not found in component version %s", name, cv.Component.Name) +} + +// GetComponentReference retrieves a component reference by its name from the component version. +func (cv *ComponentVersion) GetComponentReference(name string) (*ComponentReference, error) { + for _, ref := range cv.Component.ComponentReferences { + if ref.Name == name { + return &ref, nil + } + } + return nil, fmt.Errorf("component reference %s not found in component version %s", name, cv.Component.Name) +} + +// 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, + } + + if ocmConfig != NoOcmConfig { + flags = append(flags, "--config", ocmConfig) + } + + cmd := exec.CommandContext(ctx, "ocm", flags...) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error executing ocm command: %w", err) + } + + var cv ComponentVersion + err = yaml.Unmarshal(out, &cv) + if err != nil { + return nil, fmt.Errorf("error unmarshalling component version: %w", err) + } + + return &cv, nil +} From 751b588149307a60f1c1d20f93c7ea8c9e468122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 30 Jul 2025 16:45:29 +0200 Subject: [PATCH 2/3] add documentation --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ad6fd83..b7f2630 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,42 @@ [![REUSE status](https://api.reuse.software/badge/github.com/openmcp-project/bootstrapper)](https://api.reuse.software/info/github.com/openmcp-project/bootstrapper) -# bootstrapper +# openmcp bootstrapper ## About this project 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` + +The `ocmTransfer` command is used to transfer an OCM component version from a source location to a destination 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. + +Optional parameters: +* `--config`: Path to the OCM configuration file. + +```shell +bootstrapper ocmTransfer --source --destination --config +``` + +This command internally calls the OCM cli with the following command and arguments: + +```shell +ocm transfer componentversion --recursive --copy-resources --copy-sources --config +``` + +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 +``` + + + ## Requirements and Setup This project uses the [cobra library](https://github.com/spf13/cobra) for command line parsing. From 02aaf33f5da0728061438a2fd4bc99e67ba1dc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 30 Jul 2025 17:42:57 +0200 Subject: [PATCH 3/3] add tests --- cmd/ocmTransfer.go | 2 +- cmd/ocmTransfer_test.go | 61 +++++++++++++++++++ cmd/root.go | 8 +-- cmd/testdata/component-constructor.yaml | 5 ++ go.mod | 3 + go.sum | 7 +++ renovate.json | 5 +- test/utils/ocm.go | 79 +++++++++++++++++++++++++ 8 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 cmd/ocmTransfer_test.go create mode 100644 cmd/testdata/component-constructor.yaml create mode 100644 test/utils/ocm.go diff --git a/cmd/ocmTransfer.go b/cmd/ocmTransfer.go index f313b58..d102cb4 100644 --- a/cmd/ocmTransfer.go +++ b/cmd/ocmTransfer.go @@ -38,7 +38,7 @@ var ocmTransferCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(ocmTransferCmd) + RootCmd.AddCommand(ocmTransferCmd) ocmTransferCmd.PersistentFlags().StringP("config", "c", "", "ocm configuration file") } diff --git a/cmd/ocmTransfer_test.go b/cmd/ocmTransfer_test.go new file mode 100644 index 0000000..c65f1b2 --- /dev/null +++ b/cmd/ocmTransfer_test.go @@ -0,0 +1,61 @@ +package cmd_test + +import ( + "errors" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/openmcp-project/bootstrapper/cmd" + testutil "github.com/openmcp-project/bootstrapper/test/utils" +) + +func TestOcmTransfer(t *testing.T) { + expectError := errors.New("expected error") + + testutil.DownloadOCMAndAddToPath(t) + + ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t) + ctfOut := filepath.Join(t.TempDir(), "ctfOut") + + testCases := []struct { + desc string + arguments []string + expectedError error + }{ + { + desc: "No arguments specified", + arguments: []string{}, + expectedError: expectError, + }, + { + desc: "One argument specified", + arguments: []string{"source"}, + expectedError: expectError, + }, + { + desc: "Two arguments specified", + arguments: []string{ctfIn, ctfOut}, + expectedError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + root := cmd.RootCmd + args := []string{"ocmTransfer"} + if len(tc.arguments) > 0 { + args = append(args, tc.arguments...) + } + root.SetArgs(args) + + err := root.Execute() + if tc.expectedError != nil { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index f640773..1d593ee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ Use: "bootstrapper", Short: "A brief description of your application", Long: `A longer description that spans multiple lines and likely contains @@ -24,7 +24,7 @@ to quickly create a Cobra application.`, // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - err := rootCmd.Execute() + err := RootCmd.Execute() if err != nil { os.Exit(1) } @@ -39,5 +39,5 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/testdata/component-constructor.yaml b/cmd/testdata/component-constructor.yaml new file mode 100644 index 0000000..cc42b5f --- /dev/null +++ b/cmd/testdata/component-constructor.yaml @@ -0,0 +1,5 @@ +components: + - name: github.com/openmcp-project/bootstrapper/test + version: v0.0.1 + provider: + name: openmcp-project diff --git a/go.mod b/go.mod index ea48fc5..5d707b7 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,13 @@ 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 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect 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 ) diff --git a/go.sum b/go.sum index 8a79d0f..45e8142 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,19 @@ 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/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= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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= +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= diff --git a/renovate.json b/renovate.json index 5dbcc4d..b68e0f2 100644 --- a/renovate.json +++ b/renovate.json @@ -39,10 +39,11 @@ ], "customManagers": [ { - "description": "All component dependencies and their versions used in the Dockerfile.", + "description": "All component dependencies in other locations.", "customType": "regex", "managerFilePatterns": [ - "/Dockerfile/" + "/Dockerfile/", + "/test/util/ocm.go" ], "matchStrings": [ "# renovate: datasource=(?[a-z-.]+?) depName=(?[^\\s]+?)(?: (lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: extractVersion=(?[^\\s]+?))?(?: registryUrl=(?[^\\s]+?))?\\s.+?(_version|_VERSION)=\"?(?.+?)\"?\\s" diff --git a/test/utils/ocm.go b/test/utils/ocm.go new file mode 100644 index 0000000..9637fe6 --- /dev/null +++ b/test/utils/ocm.go @@ -0,0 +1,79 @@ +package utils + +import ( + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +const ( + // renovate: datasource=github-releases depName=ocm packageName=open-component-model/ocm + OCM_VERSION = "0.27.0" +) + +// 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) + } + defer out.Close() + + resp, err := http.Get(downloadURL) + if err != nil { + t.Fatalf("failed to download ocm: %v", err) + } + defer resp.Body.Close() + + _, 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) + } + + t.Setenv("PATH", tempDir+":"+os.Getenv("PATH")) +} + +// BuildComponent builds the component for the specified componentConstructorLocation and returns the ctf out directory. +func BuildComponent(componentConstructorLocation string, t *testing.T) string { + tempDir := t.TempDir() + ctfDir := filepath.Join(tempDir, "ctf") + + cmd := exec.Command("ocm", []string{ + "add", + "componentversions", + "--create", + "--file", + ctfDir, + componentConstructorLocation}...) + + out, err := cmd.CombinedOutput() + t.Log("OCM Output:", string(out)) + if err != nil { + t.Fatalf("failed to build component: %v", err) + } + + return ctfDir +}