Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 <source-location> --destination <destination-location> --config <path-to-ocm-config>
```

This command internally calls the OCM cli with the following command and arguments:

```shell
ocm transfer componentversion --recursive --copy-resources --copy-sources <source-location> <destination-location> --config <path-to-ocm-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.
Expand Down
44 changes: 44 additions & 0 deletions cmd/ocmTransfer.go
Original file line number Diff line number Diff line change
@@ -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")
}
61 changes: 61 additions & 0 deletions cmd/ocmTransfer_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
6 changes: 3 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
var RootCmd = &cobra.Command{
Use: "openmcp-bootstrapper",
Short: "The openMCP bootstrapper CLI",
Long: `The openMCP bootstrapper CLI is a command-line interface
Expand All @@ -20,7 +20,7 @@ for bootstrapping and updating openMCP landscapes.`,
// 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)
}
Expand All @@ -35,5 +35,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")
}
5 changes: 5 additions & 0 deletions cmd/testdata/component-constructor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
components:
- name: github.com/openmcp-project/bootstrapper/test
version: v0.0.1
provider:
name: openmcp-project
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ 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
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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +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=
140 changes: 140 additions & 0 deletions internal/ocm-cli/ocm.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 3 additions & 2 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s.+?(_version|_VERSION)=\"?(?<currentValue>.+?)\"?\\s"
Expand Down
Loading