diff --git a/internal/ocm-cli/component_getter.go b/internal/ocm-cli/component_getter.go new file mode 100644 index 0000000..7c1ed7d --- /dev/null +++ b/internal/ocm-cli/component_getter.go @@ -0,0 +1,113 @@ +package ocm_cli + +import ( + "context" + "fmt" + "strings" +) + +type ComponentGetter struct { + // Location of the root component in the format //:. + rootComponentLocation string + // Path to the deployment templates resource in the format /...//. + deploymentTemplates string + ocmConfig string + + // Fields derived during InitializeComponents + rootComponentVersion *ComponentVersion + templatesComponentVersion *ComponentVersion + templatesComponentLocation string + templatesResourceName string +} + +func NewComponentGetter(rootComponentLocation, deploymentTemplates, ocmConfig string) *ComponentGetter { + return &ComponentGetter{ + rootComponentLocation: rootComponentLocation, + deploymentTemplates: deploymentTemplates, + ocmConfig: ocmConfig, + } +} + +func (g *ComponentGetter) InitializeComponents(ctx context.Context) error { + repo, err := extractRepoFromLocation(g.rootComponentLocation) + if err != nil { + return err + } + + rootComponentVersion, err := GetComponentVersion(ctx, g.rootComponentLocation, g.ocmConfig) + if err != nil { + return fmt.Errorf("error getting root component version %s: %w", g.rootComponentLocation, err) + } + g.rootComponentVersion = rootComponentVersion + + g.deploymentTemplates = strings.TrimSpace(g.deploymentTemplates) + segments := strings.Split(g.deploymentTemplates, "/") + if len(segments) == 0 { + return fmt.Errorf("deploymentTemplates path must contain a resource name or component references and a resource name separated by slashes (ref1/.../refN/resource): %s", g.deploymentTemplates) + } + referenceNames := segments[:len(segments)-1] + g.templatesResourceName = segments[len(segments)-1] + + cv := g.rootComponentVersion + for _, refName := range referenceNames { + cv, err = getReferencedComponentVersion(ctx, repo, cv, refName, g.ocmConfig) + if err != nil { + return fmt.Errorf("error getting referenced component version %s: %w", refName, err) + } + } + + g.templatesComponentVersion = cv + g.templatesComponentLocation = buildLocation(repo, cv.Component.Name, cv.Component.Version) + return nil +} + +func (g *ComponentGetter) RootComponentVersion() *ComponentVersion { + return g.rootComponentVersion +} + +func (g *ComponentGetter) TemplatesComponentVersion() *ComponentVersion { + return g.templatesComponentVersion +} + +func (g *ComponentGetter) TemplatesResourceName() string { + return g.templatesResourceName +} + +func getReferencedComponentVersion(ctx context.Context, repo string, parentCV *ComponentVersion, refName string, ocmConfig string) (*ComponentVersion, error) { + ref, err := parentCV.GetComponentReference(refName) + if err != nil { + return nil, fmt.Errorf("error getting component reference %s: %w", refName, err) + } + + location := buildLocation(repo, ref.ComponentName, ref.Version) + cv, err := GetComponentVersion(ctx, location, ocmConfig) + if err != nil { + return nil, fmt.Errorf("error getting component version %s: %w", location, err) + } + + return cv, nil +} + +func (g *ComponentGetter) DownloadTemplatesResource(ctx context.Context, downloadDir string) error { + return downloadDirectoryResource(ctx, g.templatesComponentLocation, g.templatesResourceName, downloadDir, g.ocmConfig) +} + +func downloadDirectoryResource(ctx context.Context, componentLocation string, resourceName string, downloadDir string, ocmConfig string) error { + return Execute(ctx, + []string{"download", "resources", componentLocation, resourceName}, + []string{"--downloader", "ocm/dirtree", "--outfile", downloadDir}, + ocmConfig, + ) +} + +func extractRepoFromLocation(location string) (string, error) { + parts := strings.SplitN(location, "//", 2) + if len(parts) < 2 { + return "", fmt.Errorf("invalid component location format, expected '//:': %s", location) + } + return parts[0], nil +} + +func buildLocation(repo, name, version string) string { + return fmt.Sprintf("%s//%s:%s", repo, name, version) +} diff --git a/internal/ocm-cli/component_getter_test.go b/internal/ocm-cli/component_getter_test.go new file mode 100644 index 0000000..da4e00b --- /dev/null +++ b/internal/ocm-cli/component_getter_test.go @@ -0,0 +1,104 @@ +package ocm_cli_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" + testutil "github.com/openmcp-project/bootstrapper/test/utils" +) + +func TestComponentGetter(t *testing.T) { + const ( + componentA = "github.com/openmcp-project/bootstrapper/test-component-getter-a" + componentB = "github.com/openmcp-project/bootstrapper/test-component-getter-b" + componentC = "github.com/openmcp-project/bootstrapper/test-component-getter-c" + version001 = "v0.0.1" + templatesComponentName = "github.com/openmcp-project/gitops-templates" + ) + + testutil.DownloadOCMAndAddToPath(t) + ctf := testutil.BuildComponent("./testdata/02/component-constructor.yaml", t) + + testCases := []struct { + desc string + rootLocation string + deployTemplates string + expectInitializationError bool + expectedTemplatesComponentName string + expectResourceError bool + expectedResourceName string + }{ + { + desc: "should get a resource of the root component", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, componentA, version001), + deployTemplates: "test-resource-a", + expectedTemplatesComponentName: componentA, + expectedResourceName: "test-resource-a", + }, + { + desc: "should get a resource of a referenced component", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, componentA, version001), + deployTemplates: "reference-b/test-resource-b", + expectedTemplatesComponentName: componentB, + expectedResourceName: "test-resource-b", + }, + { + desc: "should get a resource of a nested referenced component", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, componentA, version001), + deployTemplates: "reference-b/reference-c/test-resource-c", + expectedTemplatesComponentName: componentC, + expectedResourceName: "test-resource-c", + }, + { + desc: "should fail for an unknown root component", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, "unknown-component", version001), + deployTemplates: "reference-b/test-resource-b", + expectInitializationError: true, + }, + { + desc: "should fail for an unknown component reference", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, componentA, version001), + deployTemplates: "unknown-reference/test-resource-b", + expectInitializationError: true, + }, + { + desc: "should fail for an unknown resource", + rootLocation: fmt.Sprintf("%s//%s:%s", ctf, componentA, version001), + deployTemplates: "reference-b/unknown-resource", + expectedTemplatesComponentName: componentB, + expectResourceError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + g := ocmcli.NewComponentGetter(tc.rootLocation, tc.deployTemplates, ocmcli.NoOcmConfig) + + err := g.InitializeComponents(t.Context()) + if tc.expectInitializationError { + assert.Error(t, err, "Expected an error initializing components") + return + } + assert.NoError(t, err, "Error initializing components") + + rootComponentVersion := g.RootComponentVersion() + assert.NotNil(t, rootComponentVersion, "Root component version should not be nil") + assert.Equal(t, componentA, rootComponentVersion.Component.Name, "Root component name should match") + + templatesComponentVersion := g.TemplatesComponentVersion() + assert.NotNil(t, templatesComponentVersion, "Templates component version should not be nil") + assert.Equal(t, tc.expectedTemplatesComponentName, templatesComponentVersion.Component.Name, "Templates component name should match") + + resource, err := templatesComponentVersion.GetResource(g.TemplatesResourceName()) + if tc.expectResourceError { + assert.Error(t, err, "Expected an error getting resource") + return + } + assert.NoError(t, err, "Error getting resource") + assert.Equal(t, tc.expectedResourceName, resource.Name, "Resource name should match") + }) + } +} diff --git a/internal/ocm-cli/ocm_test.go b/internal/ocm-cli/ocm_test.go index f42369c..6eaa4a6 100644 --- a/internal/ocm-cli/ocm_test.go +++ b/internal/ocm-cli/ocm_test.go @@ -16,7 +16,7 @@ func TestExecute(t *testing.T) { testutil.DownloadOCMAndAddToPath(t) - ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t) + ctfIn := testutil.BuildComponent("./testdata/01/component-constructor.yaml", t) testCases := []struct { desc string @@ -43,7 +43,7 @@ func TestExecute(t *testing.T) { desc: "get componentversion with ocm config", commands: []string{"get", "componentversion"}, arguments: []string{"--output", "yaml", ctfIn}, - ocmConfig: "./testdata/ocm-config.yaml", + ocmConfig: "./testdata/01/ocm-config.yaml", expectedError: nil, }, @@ -51,7 +51,7 @@ func TestExecute(t *testing.T) { desc: "get componentversion with unsupported ocm config", commands: []string{"get", "componentversion"}, arguments: []string{"--output", "yaml", ctfIn}, - ocmConfig: "./testdata/unsupported-ocm-config.yaml", + ocmConfig: "./testdata/01/unsupported-ocm-config.yaml", expectedError: expectError, }, } @@ -73,7 +73,7 @@ func TestGetComponentVersion(t *testing.T) { expectError := errors.New("expected error") testutil.DownloadOCMAndAddToPath(t) - ctfIn := testutil.BuildComponent("./testdata/component-constructor.yaml", t) + ctfIn := testutil.BuildComponent("./testdata/01/component-constructor.yaml", t) testCases := []struct { desc string @@ -119,13 +119,13 @@ func TestGetComponentVersion(t *testing.T) { { desc: "get component version with ocm config", componentRef: ctfIn, - ocmConfig: "./testdata/ocm-config.yaml", + ocmConfig: "./testdata/01/ocm-config.yaml", expectedError: nil, }, { desc: "get component version with unsupported ocm config", componentRef: ctfIn, - ocmConfig: "./testdata/unsupported-ocm-config.yaml", + ocmConfig: "./testdata/01/unsupported-ocm-config.yaml", expectedError: expectError, }, { diff --git a/internal/ocm-cli/testdata/component-constructor.yaml b/internal/ocm-cli/testdata/01/component-constructor.yaml similarity index 100% rename from internal/ocm-cli/testdata/component-constructor.yaml rename to internal/ocm-cli/testdata/01/component-constructor.yaml diff --git a/internal/ocm-cli/testdata/ocm-config.yaml b/internal/ocm-cli/testdata/01/ocm-config.yaml similarity index 100% rename from internal/ocm-cli/testdata/ocm-config.yaml rename to internal/ocm-cli/testdata/01/ocm-config.yaml diff --git a/internal/ocm-cli/testdata/test-resource.yaml b/internal/ocm-cli/testdata/01/test-resource.yaml similarity index 100% rename from internal/ocm-cli/testdata/test-resource.yaml rename to internal/ocm-cli/testdata/01/test-resource.yaml diff --git a/internal/ocm-cli/testdata/unsupported-ocm-config.yaml b/internal/ocm-cli/testdata/01/unsupported-ocm-config.yaml similarity index 100% rename from internal/ocm-cli/testdata/unsupported-ocm-config.yaml rename to internal/ocm-cli/testdata/01/unsupported-ocm-config.yaml diff --git a/internal/ocm-cli/testdata/02/component-constructor.yaml b/internal/ocm-cli/testdata/02/component-constructor.yaml new file mode 100644 index 0000000..e6aca88 --- /dev/null +++ b/internal/ocm-cli/testdata/02/component-constructor.yaml @@ -0,0 +1,42 @@ +components: + + - name: github.com/openmcp-project/bootstrapper/test-component-getter-a + version: v0.0.1 + provider: + name: openmcp-project + componentReferences: + - componentName: github.com/openmcp-project/bootstrapper/test-component-getter-b + name: reference-b + version: v0.0.1 + resources: + - name: test-resource-a + type: blob + input: + type: file + path: ./test-resource.yaml + + - name: github.com/openmcp-project/bootstrapper/test-component-getter-b + version: v0.0.1 + provider: + name: openmcp-project + componentReferences: + - componentName: github.com/openmcp-project/bootstrapper/test-component-getter-c + name: reference-c + version: v0.0.1 + resources: + - name: test-resource-b + type: blob + input: + type: file + path: ./test-resource.yaml + + - name: github.com/openmcp-project/bootstrapper/test-component-getter-c + version: v0.0.1 + provider: + name: openmcp-project + resources: + - name: test-resource-c + type: blob + input: + type: file + path: ./test-resource.yaml diff --git a/internal/ocm-cli/testdata/02/test-resource.yaml b/internal/ocm-cli/testdata/02/test-resource.yaml new file mode 100644 index 0000000..2ea1df8 --- /dev/null +++ b/internal/ocm-cli/testdata/02/test-resource.yaml @@ -0,0 +1,4 @@ +config: + vars: + - a: "a" + - b: "b"