From 90cfbdb076ad6055fab74b3712dc4e80e767a251 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:07:04 +0200 Subject: [PATCH 01/10] feat: add support for OCI repository pattern for add component version On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/cmd/add/component-version/cmd.go | 40 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/cli/cmd/add/component-version/cmd.go b/cli/cmd/add/component-version/cmd.go index f516a4452..1bc38839b 100644 --- a/cli/cmd/add/component-version/cmd.go +++ b/cli/cmd/add/component-version/cmd.go @@ -20,6 +20,7 @@ import ( "ocm.software/open-component-model/bindings/go/credentials" descriptor "ocm.software/open-component-model/bindings/go/descriptor/runtime" ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf" + ociv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/oci" "ocm.software/open-component-model/bindings/go/plugin/manager" "ocm.software/open-component-model/bindings/go/plugin/manager/registries/resource" "ocm.software/open-component-model/bindings/go/runtime" @@ -28,6 +29,7 @@ import ( "ocm.software/open-component-model/cli/internal/flags/enum" "ocm.software/open-component-model/cli/internal/flags/file" "ocm.software/open-component-model/cli/internal/flags/log" + "ocm.software/open-component-model/cli/internal/reference/compref" ocmsync "ocm.software/open-component-model/cli/internal/sync" ) @@ -224,15 +226,39 @@ func GetRepositorySpec(cmd *cobra.Command) (runtime.Typed, error) { if err != nil { return nil, fmt.Errorf("getting repository reference flag failed: %w", err) } - var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite - if !repoRef.Exists() { - accessMode += "|" + ctfv1.AccessModeCreate + + // Use GuessType to determine repository type + repoTypeString, err := compref.GuessType(repoRef.String()) + if err != nil { + return nil, fmt.Errorf("failed to guess repository type: %w", err) + } + + // Parse the type string into a runtime.Type + repoType, err := runtime.TypeFromString(repoTypeString) + if err != nil { + return nil, fmt.Errorf("failed to parse repository type %q: %w", repoTypeString, err) } - repoSpec := ctfv1.Repository{ - Path: repoRef.String(), - AccessMode: accessMode, + + // Create repository spec based on parsed type + switch repoType { + case runtime.NewVersionedType(ociv1.Type, ociv1.Version): + repoSpec := ociv1.Repository{ + BaseUrl: repoRef.String(), + } + return &repoSpec, nil + case runtime.NewVersionedType(ctfv1.Type, ctfv1.Version): + var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite + if !repoRef.Exists() { + accessMode += "|" + ctfv1.AccessModeCreate + } + repoSpec := ctfv1.Repository{ + Path: repoRef.String(), + AccessMode: accessMode, + } + return &repoSpec, nil + default: + return nil, fmt.Errorf("unsupported repository type: %s", repoType) } - return &repoSpec, nil } func GetComponentConstructor(file *file.Flag) (*constructorruntime.ComponentConstructor, error) { From c90ee87672699672495100972a0dd39cd8e3ca41 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:21:53 +0200 Subject: [PATCH 02/10] use the actual type after repository scheme On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/cmd/add/component-version/cmd.go | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cli/cmd/add/component-version/cmd.go b/cli/cmd/add/component-version/cmd.go index 1bc38839b..85032aa84 100644 --- a/cli/cmd/add/component-version/cmd.go +++ b/cli/cmd/add/component-version/cmd.go @@ -227,38 +227,38 @@ func GetRepositorySpec(cmd *cobra.Command) (runtime.Typed, error) { return nil, fmt.Errorf("getting repository reference flag failed: %w", err) } - // Use GuessType to determine repository type + // Get the repository type. repoTypeString, err := compref.GuessType(repoRef.String()) if err != nil { return nil, fmt.Errorf("failed to guess repository type: %w", err) } - // Parse the type string into a runtime.Type - repoType, err := runtime.TypeFromString(repoTypeString) + rtyp, err := runtime.TypeFromString(repoTypeString) if err != nil { - return nil, fmt.Errorf("failed to parse repository type %q: %w", repoTypeString, err) + return nil, fmt.Errorf("unknown type %q: %w", repoTypeString, err) } - // Create repository spec based on parsed type - switch repoType { - case runtime.NewVersionedType(ociv1.Type, ociv1.Version): - repoSpec := ociv1.Repository{ - BaseUrl: repoRef.String(), - } - return &repoSpec, nil - case runtime.NewVersionedType(ctfv1.Type, ctfv1.Version): + // create the type of the detected type. + typed, err := compref.RepositoryScheme.NewObject(rtyp) + if err != nil { + return nil, fmt.Errorf("failed to create repository of type %q: %w", repoTypeString, err) + } + + switch t := typed.(type) { + case *ociv1.Repository: + t.BaseUrl = repoRef.String() + case *ctfv1.Repository: + t.Path = repoRef.String() var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite if !repoRef.Exists() { accessMode += "|" + ctfv1.AccessModeCreate } - repoSpec := ctfv1.Repository{ - Path: repoRef.String(), - AccessMode: accessMode, - } - return &repoSpec, nil + t.AccessMode = accessMode default: - return nil, fmt.Errorf("unsupported repository type: %s", repoType) + return nil, fmt.Errorf("unsupported repository type: %T", t) } + + return typed, nil } func GetComponentConstructor(file *file.Flag) (*constructorruntime.ComponentConstructor, error) { From 588807081de7575d84507abd04e5a71f8cb61d94 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:27:01 +0200 Subject: [PATCH 03/10] add test coverage for GetRepositorySpec On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/cmd/add/component-version/cmd_test.go | 144 ++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 cli/cmd/add/component-version/cmd_test.go diff --git a/cli/cmd/add/component-version/cmd_test.go b/cli/cmd/add/component-version/cmd_test.go new file mode 100644 index 000000000..d9a112544 --- /dev/null +++ b/cli/cmd/add/component-version/cmd_test.go @@ -0,0 +1,144 @@ +package componentversion + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf" + ociv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/oci" + "ocm.software/open-component-model/cli/internal/flags/file" +) + +func TestGetRepositorySpec(t *testing.T) { + tests := []struct { + name string + repoPath string + expectedType string + validateResult func(t *testing.T, result interface{}, repoPath string) + }{ + { + name: "OCI Registry - GitHub Container Registry", + repoPath: "ghcr.io/my-org/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoPath, repo.BaseUrl) + }, + }, + { + name: "OCI Registry - localhost with port", + repoPath: "localhost:5000/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoPath, repo.BaseUrl) + }, + }, + { + name: "OCI Registry - HTTPS URL", + repoPath: "https://registry.example.com/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoPath, repo.BaseUrl) + }, + }, + { + name: "CTF Archive - relative path (non-existing)", + repoPath: "./non-existing-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoPath, repo.Path) + require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) + }, + }, + { + name: "CTF Archive - absolute path", + repoPath: "/tmp/test-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoPath, repo.Path) + require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) + }, + }, + { + name: "CTF Archive - file URL", + repoPath: "file://./local/transport-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result interface{}, repoPath string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoPath, repo.Path) + require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + cmd := &cobra.Command{Use: "test"} + file.VarP(cmd.Flags(), FlagRepositoryRef, "r", "default", "repository path") + r.NoError(cmd.Flags().Set(FlagRepositoryRef, tt.repoPath)) + + result, err := GetRepositorySpec(cmd) + r.NoError(err, "unexpected error: %v", err) + r.NotNil(result, "expected non-nil result") + + resultType := getTypeName(result) + r.Equal(tt.expectedType, resultType, "unexpected repository type") + tt.validateResult(t, result, tt.repoPath) + }) + } +} + +func TestGetRepositorySpecErrorCases(t *testing.T) { + tests := []struct { + name string + setupCmd func(*cobra.Command) error + expectedError string + }{ + { + name: "missing repository flag", + setupCmd: func(cmd *cobra.Command) error { + return nil + }, + expectedError: "getting repository reference flag failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + cmd := &cobra.Command{Use: "test"} + r.NoError(tt.setupCmd(cmd)) + + result, err := GetRepositorySpec(cmd) + r.Error(err, "expected error but got none") + r.Nil(result, "expected nil result on error") + r.Contains(err.Error(), tt.expectedError, "unexpected error message") + }) + } +} + +func getTypeName(v interface{}) string { + switch v.(type) { + case *ociv1.Repository: + return "*oci.Repository" + case *ctfv1.Repository: + return "*ctf.Repository" + default: + return "unknown" + } +} From eef81737ea99b24e9846f52f185461bab9eee8f9 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:09:29 +0200 Subject: [PATCH 04/10] the beginnings of a refactor to not duplicate logic over reference parsing On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/cmd/add/component-version/cmd.go | 37 +++------- cli/cmd/add/component-version/cmd_test.go | 3 +- .../reference/ocm_add_component-version.md | 2 +- cli/internal/reference/compref/compref.go | 69 +++++++++++++++---- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/cli/cmd/add/component-version/cmd.go b/cli/cmd/add/component-version/cmd.go index 85032aa84..74d3991fa 100644 --- a/cli/cmd/add/component-version/cmd.go +++ b/cli/cmd/add/component-version/cmd.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log/slog" + "os" "path/filepath" "strings" "time" @@ -20,7 +21,6 @@ import ( "ocm.software/open-component-model/bindings/go/credentials" descriptor "ocm.software/open-component-model/bindings/go/descriptor/runtime" ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf" - ociv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/oci" "ocm.software/open-component-model/bindings/go/plugin/manager" "ocm.software/open-component-model/bindings/go/plugin/manager/registries/resource" "ocm.software/open-component-model/bindings/go/runtime" @@ -37,7 +37,6 @@ const ( FlagConcurrencyLimit = "concurrency-limit" FlagRepositoryRef = "repository" FlagComponentConstructorPath = "constructor" - FlagCopyResources = "copy-resources" FlagBlobCacheDirectory = "blob-cache-directory" FlagComponentVersionConflictPolicy = "component-version-conflict-policy" FlagSkipReferenceDigestProcessing = "skip-reference-digest-processing" @@ -108,7 +107,7 @@ add component-version --%[1]s ./path/to/%[2]s --%[3]s ./path/to/%[4]s.yaml } cmd.Flags().Int(FlagConcurrencyLimit, 4, "maximum number of component versions that can be constructed concurrently.") - file.VarP(cmd.Flags(), FlagRepositoryRef, string(FlagRepositoryRef[0]), LegacyDefaultArchiveName, "path to the repository") + cmd.Flags().StringP(FlagRepositoryRef, string(FlagRepositoryRef[0]), LegacyDefaultArchiveName, "repository specification") file.VarP(cmd.Flags(), FlagComponentConstructorPath, string(FlagComponentConstructorPath[0]), DefaultComponentConstructorBaseName+".yaml", "path to the component constructor file") cmd.Flags().String(FlagBlobCacheDirectory, filepath.Join(".ocm", "cache"), "path to the blob cache directory") enum.Var(cmd.Flags(), FlagComponentVersionConflictPolicy, ComponentVersionConflictPolicies(), "policy to apply when a component version already exists in the repository") @@ -222,40 +221,24 @@ func AddComponentVersion(cmd *cobra.Command, _ []string) error { } func GetRepositorySpec(cmd *cobra.Command) (runtime.Typed, error) { - repoRef, err := file.Get(cmd.Flags(), FlagRepositoryRef) + repositorySpec, err := cmd.Flags().GetString(FlagRepositoryRef) if err != nil { return nil, fmt.Errorf("getting repository reference flag failed: %w", err) } - // Get the repository type. - repoTypeString, err := compref.GuessType(repoRef.String()) + typed, err := compref.ParseRepository(repositorySpec) if err != nil { - return nil, fmt.Errorf("failed to guess repository type: %w", err) + return nil, fmt.Errorf("failed to parse repository: %w", err) } - rtyp, err := runtime.TypeFromString(repoTypeString) - if err != nil { - return nil, fmt.Errorf("unknown type %q: %w", repoTypeString, err) - } - - // create the type of the detected type. - typed, err := compref.RepositoryScheme.NewObject(rtyp) - if err != nil { - return nil, fmt.Errorf("failed to create repository of type %q: %w", repoTypeString, err) - } - - switch t := typed.(type) { - case *ociv1.Repository: - t.BaseUrl = repoRef.String() - case *ctfv1.Repository: - t.Path = repoRef.String() + // Handle CTF-specific access mode configuration + if ctfRepo, ok := typed.(*ctfv1.Repository); ok { var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite - if !repoRef.Exists() { + // For CTF repositories, check if path exists to determine access mode + if _, err := os.Stat(ctfRepo.Path); os.IsNotExist(err) { accessMode += "|" + ctfv1.AccessModeCreate } - t.AccessMode = accessMode - default: - return nil, fmt.Errorf("unsupported repository type: %T", t) + ctfRepo.AccessMode = accessMode } return typed, nil diff --git a/cli/cmd/add/component-version/cmd_test.go b/cli/cmd/add/component-version/cmd_test.go index d9a112544..6f182332e 100644 --- a/cli/cmd/add/component-version/cmd_test.go +++ b/cli/cmd/add/component-version/cmd_test.go @@ -8,7 +8,6 @@ import ( ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf" ociv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/oci" - "ocm.software/open-component-model/cli/internal/flags/file" ) func TestGetRepositorySpec(t *testing.T) { @@ -88,7 +87,7 @@ func TestGetRepositorySpec(t *testing.T) { r := require.New(t) cmd := &cobra.Command{Use: "test"} - file.VarP(cmd.Flags(), FlagRepositoryRef, "r", "default", "repository path") + cmd.Flags().StringP(FlagRepositoryRef, "r", "default", "repository specification") r.NoError(cmd.Flags().Set(FlagRepositoryRef, tt.repoPath)) result, err := GetRepositorySpec(cmd) diff --git a/cli/docs/reference/ocm_add_component-version.md b/cli/docs/reference/ocm_add_component-version.md index f668dc96e..cec5d201d 100644 --- a/cli/docs/reference/ocm_add_component-version.md +++ b/cli/docs/reference/ocm_add_component-version.md @@ -50,7 +50,7 @@ add component-version --repository ./path/to/transport-archive --constructor ./ --concurrency-limit int maximum number of component versions that can be constructed concurrently. (default 4) -c, --constructor path path to the component constructor file (default component-constructor.yaml) -h, --help help for component-version - -r, --repository path path to the repository (default transport-archive) + -r, --repository string repository specification (default "transport-archive") --skip-reference-digest-processing skip digest processing for resources and sources. Any resource referenced via access type will not have their digest updated. ``` diff --git a/cli/internal/reference/compref/compref.go b/cli/internal/reference/compref/compref.go index 876dfa6f0..74c965057 100644 --- a/cli/internal/reference/compref/compref.go +++ b/cli/internal/reference/compref/compref.go @@ -185,25 +185,70 @@ func Parse(input string) (*Ref, error) { return nil, fmt.Errorf("invalid component name %q in %q, must match %q", ref.Component, originalInput, ComponentRegex) } - // Step 6: Resolve type if not explicitly given + // Step 6: Build repository object using ParseRepository + var repositorySpec string + if ref.Type != "" { + repositorySpec = ref.Type + "::" + input + } else { + repositorySpec = input + } + + repository, err := ParseRepository(repositorySpec) + if err != nil { + return nil, fmt.Errorf("failed to parse repository: %w", err) + } + + ref.Repository = repository + + // Extract the type from the parsed repository for consistency if ref.Type == "" { - t, err := GuessType(input) + switch repository.(type) { + case *ociv1.Repository: + ref.Type = runtime.NewVersionedType(ociv1.Type, ociv1.Version).String() + case *ctfv1.Repository: + ref.Type = runtime.NewVersionedType(ctfv1.Type, ctfv1.Version).String() + } + } + + return ref, nil +} + +// ParseRepository parses a repository specification string and returns a typed repository object. +// It accepts repository strings in the format: +// - [::] +// +// Where type can be "ctf" or "oci", and repository-spec is the actual repository location. +// If no type is specified, it will be guessed using heuristics. +func ParseRepository(repositorySpec string) (runtime.Typed, error) { + originalInput := repositorySpec + input := repositorySpec + + // Extract optional type + var repoType string + if idx := strings.Index(input, "::"); idx != -1 { + repoType = input[:idx] + input = input[idx+2:] + } + + // Resolve type if isn't explicitly given + if repoType == "" { + t, err := guessType(input) if err != nil { return nil, fmt.Errorf("failed to detect repository type from %q: %w", input, err) } Base.Debug("ocm had to guess your repository type", "type", t, "input", input) - ref.Type = t + repoType = t } - // Step 7: Build repository object - rtyp, err := runtime.TypeFromString(ref.Type) + // Build repository object + rtyp, err := runtime.TypeFromString(repoType) if err != nil { - return nil, fmt.Errorf("unknown type %q in %q: %w", ref.Type, originalInput, err) + return nil, fmt.Errorf("unknown type %q in %q: %w", repoType, originalInput, err) } typed, err := RepositoryScheme.NewObject(rtyp) if err != nil { - return nil, fmt.Errorf("failed to create repository of type %q: %w", ref.Type, err) + return nil, fmt.Errorf("failed to create repository of type %q: %w", repoType, err) } switch t := typed.(type) { @@ -216,15 +261,13 @@ func Parse(input string) (*Ref, error) { case *ctfv1.Repository: t.Path = input default: - return nil, fmt.Errorf("unsupported repository type: %q", ref.Type) + return nil, fmt.Errorf("unsupported repository type: %q", repoType) } - ref.Repository = typed - - return ref, nil + return typed, nil } -// GuessType tries to guess the repository type ("ctf" or "oci") +// guessType tries to guess the repository type ("ctf" or "oci") // from an untyped repository specification string. // // You may ask yourself why this is needed. @@ -239,7 +282,7 @@ func Parse(input string) (*Ref, error) { // - If it looks like a domain (contains dots like ".com", ".io", etc.), assume OCI // - If it contains a colon (e.g., "localhost:5000"), assume OCI // - Otherwise fallback to CTF -func GuessType(repository string) (string, error) { +func guessType(repository string) (string, error) { // Try parsing as URL first if u, err := url.Parse(repository); err == nil { if u.Scheme == "file" { From b865ec5cd03eeaec78261149e291fee3e1904d68 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:37:55 +0200 Subject: [PATCH 05/10] shifted the tests into compdesc On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/cmd/add/component-version/cmd_test.go | 143 ----------------- .../reference/compref/compref_test.go | 145 ++++++++++++++++++ 2 files changed, 145 insertions(+), 143 deletions(-) delete mode 100644 cli/cmd/add/component-version/cmd_test.go diff --git a/cli/cmd/add/component-version/cmd_test.go b/cli/cmd/add/component-version/cmd_test.go deleted file mode 100644 index 6f182332e..000000000 --- a/cli/cmd/add/component-version/cmd_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package componentversion - -import ( - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/require" - - ctfv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/ctf" - ociv1 "ocm.software/open-component-model/bindings/go/oci/spec/repository/v1/oci" -) - -func TestGetRepositorySpec(t *testing.T) { - tests := []struct { - name string - repoPath string - expectedType string - validateResult func(t *testing.T, result interface{}, repoPath string) - }{ - { - name: "OCI Registry - GitHub Container Registry", - repoPath: "ghcr.io/my-org/my-repo", - expectedType: "*oci.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ociv1.Repository) - require.True(t, ok, "expected *ociv1.Repository") - require.Equal(t, repoPath, repo.BaseUrl) - }, - }, - { - name: "OCI Registry - localhost with port", - repoPath: "localhost:5000/my-repo", - expectedType: "*oci.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ociv1.Repository) - require.True(t, ok, "expected *ociv1.Repository") - require.Equal(t, repoPath, repo.BaseUrl) - }, - }, - { - name: "OCI Registry - HTTPS URL", - repoPath: "https://registry.example.com/my-repo", - expectedType: "*oci.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ociv1.Repository) - require.True(t, ok, "expected *ociv1.Repository") - require.Equal(t, repoPath, repo.BaseUrl) - }, - }, - { - name: "CTF Archive - relative path (non-existing)", - repoPath: "./non-existing-archive", - expectedType: "*ctf.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ctfv1.Repository) - require.True(t, ok, "expected *ctfv1.Repository") - require.Equal(t, repoPath, repo.Path) - require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) - }, - }, - { - name: "CTF Archive - absolute path", - repoPath: "/tmp/test-archive", - expectedType: "*ctf.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ctfv1.Repository) - require.True(t, ok, "expected *ctfv1.Repository") - require.Equal(t, repoPath, repo.Path) - require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) - }, - }, - { - name: "CTF Archive - file URL", - repoPath: "file://./local/transport-archive", - expectedType: "*ctf.Repository", - validateResult: func(t *testing.T, result interface{}, repoPath string) { - repo, ok := result.(*ctfv1.Repository) - require.True(t, ok, "expected *ctfv1.Repository") - require.Equal(t, repoPath, repo.Path) - require.Equal(t, ctfv1.AccessMode(ctfv1.AccessModeReadWrite+"|"+ctfv1.AccessModeCreate), repo.AccessMode) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := require.New(t) - - cmd := &cobra.Command{Use: "test"} - cmd.Flags().StringP(FlagRepositoryRef, "r", "default", "repository specification") - r.NoError(cmd.Flags().Set(FlagRepositoryRef, tt.repoPath)) - - result, err := GetRepositorySpec(cmd) - r.NoError(err, "unexpected error: %v", err) - r.NotNil(result, "expected non-nil result") - - resultType := getTypeName(result) - r.Equal(tt.expectedType, resultType, "unexpected repository type") - tt.validateResult(t, result, tt.repoPath) - }) - } -} - -func TestGetRepositorySpecErrorCases(t *testing.T) { - tests := []struct { - name string - setupCmd func(*cobra.Command) error - expectedError string - }{ - { - name: "missing repository flag", - setupCmd: func(cmd *cobra.Command) error { - return nil - }, - expectedError: "getting repository reference flag failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := require.New(t) - - cmd := &cobra.Command{Use: "test"} - r.NoError(tt.setupCmd(cmd)) - - result, err := GetRepositorySpec(cmd) - r.Error(err, "expected error but got none") - r.Nil(result, "expected nil result on error") - r.Contains(err.Error(), tt.expectedError, "unexpected error message") - }) - } -} - -func getTypeName(v interface{}) string { - switch v.(type) { - case *ociv1.Repository: - return "*oci.Repository" - case *ctfv1.Repository: - return "*ctf.Repository" - default: - return "unknown" - } -} diff --git a/cli/internal/reference/compref/compref_test.go b/cli/internal/reference/compref/compref_test.go index ece040a51..792d2b1d7 100644 --- a/cli/internal/reference/compref/compref_test.go +++ b/cli/internal/reference/compref/compref_test.go @@ -382,3 +382,148 @@ func Test_ComponentReference_Permutations(t *testing.T) { } } } + +func TestParseRepository(t *testing.T) { + tests := []struct { + name string + repoSpec string + expectedType string + validateResult func(t *testing.T, result runtime.Typed, repoSpec string) + }{ + { + name: "OCI Registry - GitHub Container Registry", + repoSpec: "ghcr.io/my-org/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoSpec, repo.BaseUrl) + }, + }, + { + name: "OCI Registry - localhost with port", + repoSpec: "localhost:5000/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoSpec, repo.BaseUrl) + }, + }, + { + name: "OCI Registry - HTTPS URL", + repoSpec: "https://registry.example.com/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, repoSpec, repo.BaseUrl) + }, + }, + { + name: "CTF Archive - relative path", + repoSpec: "./non-existing-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoSpec, repo.Path) + }, + }, + { + name: "CTF Archive - absolute path", + repoSpec: "/tmp/test-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoSpec, repo.Path) + }, + }, + { + name: "CTF Archive - file URL", + repoSpec: "file://./local/transport-archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, repoSpec, repo.Path) + }, + }, + { + name: "OCI Registry with explicit type", + repoSpec: "oci::ghcr.io/my-org/my-repo", + expectedType: "*oci.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ociv1.Repository) + require.True(t, ok, "expected *ociv1.Repository") + require.Equal(t, "ghcr.io/my-org/my-repo", repo.BaseUrl) + }, + }, + { + name: "CTF Archive with explicit type", + repoSpec: "ctf::./local/archive", + expectedType: "*ctf.Repository", + validateResult: func(t *testing.T, result runtime.Typed, repoSpec string) { + repo, ok := result.(*ctfv1.Repository) + require.True(t, ok, "expected *ctfv1.Repository") + require.Equal(t, "./local/archive", repo.Path) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + result, err := ParseRepository(tt.repoSpec) + r.NoError(err, "unexpected error: %v", err) + r.NotNil(result, "expected non-nil result") + + resultType := getRepositoryTypeName(result) + r.Equal(tt.expectedType, resultType, "unexpected repository type") + tt.validateResult(t, result, tt.repoSpec) + }) + } +} + +func TestParseRepositoryErrorCases(t *testing.T) { + tests := []struct { + name string + repoSpec string + expectedError string + }{ + { + name: "unknown type", + repoSpec: "unknown::some-repo", + expectedError: "unsupported repository type", + }, + { + name: "invalid type format", + repoSpec: "invalid-type-format::some-repo", + expectedError: "unsupported repository type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := require.New(t) + + result, err := ParseRepository(tt.repoSpec) + r.Error(err, "expected error but got none") + r.Nil(result, "expected nil result on error") + r.Contains(err.Error(), tt.expectedError, "unexpected error message") + }) + } +} + +func getRepositoryTypeName(v runtime.Typed) string { + switch v.(type) { + case *ociv1.Repository: + return "*oci.Repository" + case *ctfv1.Repository: + return "*ctf.Repository" + default: + return "unknown" + } +} From 65856caa1fdb115f5d20e27aab0865713c3f6c40 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:47:57 +0200 Subject: [PATCH 06/10] add integration test for add component version On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- .../add_component_version_integration_test.go | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 cli/integration/add_component_version_integration_test.go diff --git a/cli/integration/add_component_version_integration_test.go b/cli/integration/add_component_version_integration_test.go new file mode 100644 index 000000000..c79eb53b3 --- /dev/null +++ b/cli/integration/add_component_version_integration_test.go @@ -0,0 +1,290 @@ +package integration + +import ( + "fmt" + "net" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "ocm.software/open-component-model/bindings/go/oci" + urlresolver "ocm.software/open-component-model/bindings/go/oci/resolver/url" + "ocm.software/open-component-model/cli/cmd" + "ocm.software/open-component-model/cli/integration/internal" +) + +func Test_Integration_AddComponentVersion_OCIRepository(t *testing.T) { + r := require.New(t) + t.Parallel() + + t.Logf("Starting OCI repository add component-version integration test") + user := "ocm" + + // Setup credentials and htpasswd + password := internal.GenerateRandomPassword(t, 20) + htpasswd := internal.GenerateHtpasswd(t, user, password) + + // Start containerized registry + registryAddress := internal.StartDockerContainerRegistry(t, htpasswd) + host, port, err := net.SplitHostPort(registryAddress) + r.NoError(err) + + cfg := fmt.Sprintf(` +type: generic.config.ocm.software/v1 +configurations: +- type: credentials.config.ocm.software + consumers: + - identity: + type: OCIRepository/v1 + hostname: %[1]q + port: %[2]q + scheme: http + credentials: + - type: Credentials/v1 + properties: + username: %[3]q + password: %[4]q +`, host, port, user, password) + cfgPath := filepath.Join(t.TempDir(), "ocmconfig.yaml") + r.NoError(os.WriteFile(cfgPath, []byte(cfg), os.ModePerm)) + + t.Logf("Generated config:\n%s", cfg) + + client := internal.CreateAuthClient(registryAddress, user, password) + + resolver, err := urlresolver.New( + urlresolver.WithBaseURL(registryAddress), + urlresolver.WithPlainHTTP(true), + urlresolver.WithBaseClient(client), + ) + r.NoError(err) + + repo, err := oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(t.TempDir())) + r.NoError(err) + + t.Run("add component-version with plain OCI registry reference", func(t *testing.T) { + r := require.New(t) + + componentName := "ocm.software/test-component" + componentVersion := "v1.0.0" + + // Create constructor file + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: test-resource + version: v1.0.0 + type: plainText + input: + type: utf8 + text: "Hello, World from OCI registry!" +`, componentName, componentVersion) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + + // Test the add component-version command with plain OCI registry reference + addCMD := cmd.New() + addCMD.SetArgs([]string{ + "add", + "component-version", + "--repository", registryAddress, + "--constructor", constructorPath, + "--config", cfgPath, + }) + + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with OCI registry") + + // Verify the component version was added by attempting to retrieve it + desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) + r.NoError(err, "should be able to retrieve the added component version") + r.Equal(componentName, desc.Component.Name) + r.Equal(componentVersion, desc.Component.Version) + r.Equal("ocm.software", desc.Component.Provider.Name) + r.Len(desc.Component.Resources, 1) + r.Equal("test-resource", desc.Component.Resources[0].Name) + r.Equal("v1.0.0", desc.Component.Resources[0].Version) + }) + + t.Run("add component-version with explicit OCI type prefix", func(t *testing.T) { + r := require.New(t) + + componentName := "ocm.software/explicit-oci-component" + componentVersion := "v2.0.0" + + // Create constructor file + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: explicit-resource + version: v2.0.0 + type: plainText + input: + type: utf8 + text: "Hello from explicit OCI type!" +`, componentName, componentVersion) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + + // Test with explicit oci:: prefix + addCMD := cmd.New() + addCMD.SetArgs([]string{ + "add", + "component-version", + "--repository", fmt.Sprintf("oci::%s", registryAddress), + "--constructor", constructorPath, + "--config", cfgPath, + }) + + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with explicit OCI type") + + // Verify the component version was added + desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) + r.NoError(err, "should be able to retrieve the component version added with explicit OCI type") + r.Equal(componentName, desc.Component.Name) + r.Equal(componentVersion, desc.Component.Version) + }) + + t.Run("add component-version with HTTPS URL format", func(t *testing.T) { + r := require.New(t) + + componentName := "ocm.software/https-component" + componentVersion := "v3.0.0" + + // Create constructor file + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: https-resource + version: v3.0.0 + type: plainText + input: + type: utf8 + text: "Hello from HTTPS URL format!" +`, componentName, componentVersion) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + + // Test with HTTPS URL format (will be treated as HTTP due to plain HTTP resolver) + addCMD := cmd.New() + addCMD.SetArgs([]string{ + "add", + "component-version", + "--repository", fmt.Sprintf("http://%s", registryAddress), + "--constructor", constructorPath, + "--config", cfgPath, + }) + + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with HTTPS URL format") + + // Verify the component version was added + desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) + r.NoError(err, "should be able to retrieve the component version added with HTTPS URL") + r.Equal(componentName, desc.Component.Name) + r.Equal(componentVersion, desc.Component.Version) + }) +} + +func Test_Integration_AddComponentVersion_CTFRepository(t *testing.T) { + t.Parallel() + + t.Run("add component-version with CTF archive path", func(t *testing.T) { + r := require.New(t) + + componentName := "ocm.software/ctf-component" + componentVersion := "v1.0.0" + + // Create constructor file + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: ctf-resource + version: v1.0.0 + type: plainText + input: + type: utf8 + text: "Hello from CTF archive!" +`, componentName, componentVersion) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + + // Test with CTF archive path + ctfArchivePath := filepath.Join(t.TempDir(), "test-archive") + addCMD := cmd.New() + addCMD.SetArgs([]string{ + "add", + "component-version", + "--repository", ctfArchivePath, + "--constructor", constructorPath, + }) + + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with CTF archive") + + // Verify the archive was created + _, err := os.Stat(ctfArchivePath) + r.NoError(err, "CTF archive should be created") + }) + + t.Run("add component-version with explicit CTF type prefix", func(t *testing.T) { + r := require.New(t) + + componentName := "ocm.software/explicit-ctf-component" + componentVersion := "v2.0.0" + + // Create constructor file + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: explicit-ctf-resource + version: v2.0.0 + type: plainText + input: + type: utf8 + text: "Hello from explicit CTF type!" +`, componentName, componentVersion) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + + // Test with explicit ctf:: prefix + ctfArchivePath := filepath.Join(t.TempDir(), "explicit-archive") + addCMD := cmd.New() + addCMD.SetArgs([]string{ + "add", + "component-version", + "--repository", fmt.Sprintf("ctf::%s", ctfArchivePath), + "--constructor", constructorPath, + }) + + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with explicit CTF type") + + // Verify the archive was created + _, err := os.Stat(ctfArchivePath) + r.NoError(err, "CTF archive should be created with explicit type") + }) +} \ No newline at end of file From 1be27780dfda6437d57bc4154597e380eaa76492 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:57:13 +0200 Subject: [PATCH 07/10] add scheme to the repository spec for credentials to work On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/integration/add_component_version_integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/integration/add_component_version_integration_test.go b/cli/integration/add_component_version_integration_test.go index c79eb53b3..7736cc1b9 100644 --- a/cli/integration/add_component_version_integration_test.go +++ b/cli/integration/add_component_version_integration_test.go @@ -94,7 +94,7 @@ components: addCMD.SetArgs([]string{ "add", "component-version", - "--repository", registryAddress, + "--repository", fmt.Sprintf("http://%s", registryAddress), "--constructor", constructorPath, "--config", cfgPath, }) @@ -142,7 +142,7 @@ components: addCMD.SetArgs([]string{ "add", "component-version", - "--repository", fmt.Sprintf("oci::%s", registryAddress), + "--repository", fmt.Sprintf("oci::http://%s", registryAddress), "--constructor", constructorPath, "--config", cfgPath, }) From 476c932005366a1cca39b9a5e4fd3fe03717ee80 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:52:36 +0200 Subject: [PATCH 08/10] tests are now passing I believe On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- .../add_component_version_integration_test.go | 186 +++--------------- .../download_resource_integration_test.go | 67 +------ cli/integration/suite_test.go | 152 ++++++++++++++ 3 files changed, 190 insertions(+), 215 deletions(-) create mode 100644 cli/integration/suite_test.go diff --git a/cli/integration/add_component_version_integration_test.go b/cli/integration/add_component_version_integration_test.go index 7736cc1b9..9a2279fd2 100644 --- a/cli/integration/add_component_version_integration_test.go +++ b/cli/integration/add_component_version_integration_test.go @@ -2,67 +2,17 @@ package integration import ( "fmt" - "net" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" - "ocm.software/open-component-model/bindings/go/oci" - urlresolver "ocm.software/open-component-model/bindings/go/oci/resolver/url" "ocm.software/open-component-model/cli/cmd" - "ocm.software/open-component-model/cli/integration/internal" ) func Test_Integration_AddComponentVersion_OCIRepository(t *testing.T) { - r := require.New(t) - t.Parallel() - - t.Logf("Starting OCI repository add component-version integration test") - user := "ocm" - - // Setup credentials and htpasswd - password := internal.GenerateRandomPassword(t, 20) - htpasswd := internal.GenerateHtpasswd(t, user, password) - - // Start containerized registry - registryAddress := internal.StartDockerContainerRegistry(t, htpasswd) - host, port, err := net.SplitHostPort(registryAddress) - r.NoError(err) - - cfg := fmt.Sprintf(` -type: generic.config.ocm.software/v1 -configurations: -- type: credentials.config.ocm.software - consumers: - - identity: - type: OCIRepository/v1 - hostname: %[1]q - port: %[2]q - scheme: http - credentials: - - type: Credentials/v1 - properties: - username: %[3]q - password: %[4]q -`, host, port, user, password) - cfgPath := filepath.Join(t.TempDir(), "ocmconfig.yaml") - r.NoError(os.WriteFile(cfgPath, []byte(cfg), os.ModePerm)) - - t.Logf("Generated config:\n%s", cfg) - - client := internal.CreateAuthClient(registryAddress, user, password) - - resolver, err := urlresolver.New( - urlresolver.WithBaseURL(registryAddress), - urlresolver.WithPlainHTTP(true), - urlresolver.WithBaseClient(client), - ) - r.NoError(err) - - repo, err := oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(t.TempDir())) - r.NoError(err) + suite := SetupTestSuite(t) t.Run("add component-version with plain OCI registry reference", func(t *testing.T) { r := require.New(t) @@ -70,46 +20,30 @@ configurations: componentName := "ocm.software/test-component" componentVersion := "v1.0.0" - // Create constructor file - constructorContent := fmt.Sprintf(` -components: -- name: %s - version: %s - provider: - name: ocm.software - resources: - - name: test-resource - version: v1.0.0 - type: plainText - input: - type: utf8 - text: "Hello, World from OCI registry!" -`, componentName, componentVersion) - - constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") - r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + // Create constructor file using suite helper + constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion) // Test the add component-version command with plain OCI registry reference addCMD := cmd.New() addCMD.SetArgs([]string{ "add", "component-version", - "--repository", fmt.Sprintf("http://%s", registryAddress), + "--repository", suite.GetRepositoryURL(), "--constructor", constructorPath, - "--config", cfgPath, + "--config", suite.ConfigPath, }) r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with OCI registry") // Verify the component version was added by attempting to retrieve it - desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) + desc, err := suite.Repository.GetComponentVersion(t.Context(), componentName, componentVersion) r.NoError(err, "should be able to retrieve the added component version") r.Equal(componentName, desc.Component.Name) r.Equal(componentVersion, desc.Component.Version) r.Equal("ocm.software", desc.Component.Provider.Name) r.Len(desc.Component.Resources, 1) r.Equal("test-resource", desc.Component.Resources[0].Name) - r.Equal("v1.0.0", desc.Component.Resources[0].Version) + r.Equal(componentVersion, desc.Component.Resources[0].Version) }) t.Run("add component-version with explicit OCI type prefix", func(t *testing.T) { @@ -118,116 +52,67 @@ components: componentName := "ocm.software/explicit-oci-component" componentVersion := "v2.0.0" - // Create constructor file - constructorContent := fmt.Sprintf(` -components: -- name: %s - version: %s - provider: - name: ocm.software - resources: - - name: explicit-resource - version: v2.0.0 - type: plainText - input: - type: utf8 - text: "Hello from explicit OCI type!" -`, componentName, componentVersion) - - constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") - r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + // Create constructor file using suite helper + constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion) // Test with explicit oci:: prefix addCMD := cmd.New() addCMD.SetArgs([]string{ "add", "component-version", - "--repository", fmt.Sprintf("oci::http://%s", registryAddress), + "--repository", suite.GetRepositoryURLWithPrefix("oci"), "--constructor", constructorPath, - "--config", cfgPath, + "--config", suite.ConfigPath, }) r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with explicit OCI type") // Verify the component version was added - desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) + desc, err := suite.Repository.GetComponentVersion(t.Context(), componentName, componentVersion) r.NoError(err, "should be able to retrieve the component version added with explicit OCI type") r.Equal(componentName, desc.Component.Name) r.Equal(componentVersion, desc.Component.Version) }) - t.Run("add component-version with HTTPS URL format", func(t *testing.T) { + t.Run("add component-version with HTTP URL format", func(t *testing.T) { r := require.New(t) - componentName := "ocm.software/https-component" + componentName := "ocm.software/http-component" componentVersion := "v3.0.0" - // Create constructor file - constructorContent := fmt.Sprintf(` -components: -- name: %s - version: %s - provider: - name: ocm.software - resources: - - name: https-resource - version: v3.0.0 - type: plainText - input: - type: utf8 - text: "Hello from HTTPS URL format!" -`, componentName, componentVersion) - - constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") - r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + // Create constructor file using suite helper + constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion) - // Test with HTTPS URL format (will be treated as HTTP due to plain HTTP resolver) + // Test with HTTP URL format addCMD := cmd.New() addCMD.SetArgs([]string{ "add", "component-version", - "--repository", fmt.Sprintf("http://%s", registryAddress), + "--repository", suite.GetRepositoryURL(), "--constructor", constructorPath, - "--config", cfgPath, + "--config", suite.ConfigPath, }) - r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with HTTPS URL format") + r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with HTTP URL format") // Verify the component version was added - desc, err := repo.GetComponentVersion(t.Context(), componentName, componentVersion) - r.NoError(err, "should be able to retrieve the component version added with HTTPS URL") + desc, err := suite.Repository.GetComponentVersion(t.Context(), componentName, componentVersion) + r.NoError(err, "should be able to retrieve the component version added with HTTP URL") r.Equal(componentName, desc.Component.Name) r.Equal(componentVersion, desc.Component.Version) }) } func Test_Integration_AddComponentVersion_CTFRepository(t *testing.T) { - t.Parallel() - t.Run("add component-version with CTF archive path", func(t *testing.T) { r := require.New(t) componentName := "ocm.software/ctf-component" componentVersion := "v1.0.0" - // Create constructor file - constructorContent := fmt.Sprintf(` -components: -- name: %s - version: %s - provider: - name: ocm.software - resources: - - name: ctf-resource - version: v1.0.0 - type: plainText - input: - type: utf8 - text: "Hello from CTF archive!" -`, componentName, componentVersion) - - constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") - r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + // Create temporary test suite for this test (no shared registry needed for CTF) + suite := &TestSuite{} + constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion) // Test with CTF archive path ctfArchivePath := filepath.Join(t.TempDir(), "test-archive") @@ -252,24 +137,9 @@ components: componentName := "ocm.software/explicit-ctf-component" componentVersion := "v2.0.0" - // Create constructor file - constructorContent := fmt.Sprintf(` -components: -- name: %s - version: %s - provider: - name: ocm.software - resources: - - name: explicit-ctf-resource - version: v2.0.0 - type: plainText - input: - type: utf8 - text: "Hello from explicit CTF type!" -`, componentName, componentVersion) - - constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") - r.NoError(os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + // Create temporary test suite for this test (no shared registry needed for CTF) + suite := &TestSuite{} + constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion) // Test with explicit ctf:: prefix ctfArchivePath := filepath.Join(t.TempDir(), "explicit-archive") diff --git a/cli/integration/download_resource_integration_test.go b/cli/integration/download_resource_integration_test.go index b6299def6..7d0dea7c3 100644 --- a/cli/integration/download_resource_integration_test.go +++ b/cli/integration/download_resource_integration_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "net" "os" "os/exec" "path/filepath" @@ -23,8 +22,6 @@ import ( "ocm.software/open-component-model/bindings/go/blob/filesystem" descriptor "ocm.software/open-component-model/bindings/go/descriptor/runtime" v2 "ocm.software/open-component-model/bindings/go/descriptor/v2" - "ocm.software/open-component-model/bindings/go/oci" - urlresolver "ocm.software/open-component-model/bindings/go/oci/resolver/url" "ocm.software/open-component-model/bindings/go/oci/spec/layout" "ocm.software/open-component-model/bindings/go/repository" "ocm.software/open-component-model/cli/cmd" @@ -33,51 +30,7 @@ import ( ) func Test_Integration_OCIRepository(t *testing.T) { - r := require.New(t) - t.Parallel() - - t.Logf("Starting OCI based integration test") - user := "ocm" - - // Setup credentials and htpasswd - password := internal.GenerateRandomPassword(t, 20) - htpasswd := internal.GenerateHtpasswd(t, user, password) - - // Start containerized registry - registryAddress := internal.StartDockerContainerRegistry(t, htpasswd) - host, port, err := net.SplitHostPort(registryAddress) - r.NoError(err) - - cfg := fmt.Sprintf(` -type: generic.config.ocm.software/v1 -configurations: -- type: credentials.config.ocm.software - consumers: - - identity: - type: OCIRepository/v1 - hostname: %[1]q - port: %[2]q - scheme: http - credentials: - - type: Credentials/v1 - properties: - username: %[3]q - password: %[4]q -`, host, port, user, password) - cfgPath := filepath.Join(t.TempDir(), "ocmconfig.yaml") - r.NoError(os.WriteFile(cfgPath, []byte(cfg), os.ModePerm)) - - client := internal.CreateAuthClient(registryAddress, user, password) - - resolver, err := urlresolver.New( - urlresolver.WithBaseURL(registryAddress), - urlresolver.WithPlainHTTP(true), - urlresolver.WithBaseClient(client), - ) - r.NoError(err) - - repo, err := oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(t.TempDir())) - r.NoError(err) + suite := SetupTestSuite(t) t.Run("download resource with arbitrary byte stream data", func(t *testing.T) { r := require.New(t) @@ -99,7 +52,7 @@ configurations: name, version := "ocm.software/test-component", "v1.0.0" - uploadComponentVersion(t, repo, name, version, localResource) + uploadComponentVersion(t, suite.Repository, name, version, localResource) downloadCMD := cmd.New() @@ -108,13 +61,13 @@ configurations: downloadCMD.SetArgs([]string{ "download", "resource", - fmt.Sprintf("http://%s//%s:%s", registryAddress, name, version), + fmt.Sprintf("%s//%s:%s", suite.GetRepositoryURL(), name, version), "--identity", fmt.Sprintf("name=%s,version=%s", localResource.Resource.Name, localResource.Resource.Version), "--output", output, "--config", - cfgPath, + suite.ConfigPath, }) r.NoError(downloadCMD.ExecuteContext(t.Context())) @@ -153,9 +106,9 @@ configurations: true), } - name, version := "ocm.software/test-component", "v1.0.0" + name, version := "ocm.software/test-component-oci-layout", "v1.0.0" - uploadComponentVersion(t, repo, name, version, localResource) + uploadComponentVersion(t, suite.Repository, name, version, localResource) t.Run("download with disabled extract", func(t *testing.T) { r := require.New(t) @@ -164,13 +117,13 @@ configurations: downloadCMD.SetArgs([]string{ "download", "resource", - fmt.Sprintf("http://%s//%s:%s", registryAddress, name, version), + fmt.Sprintf("%s//%s:%s", suite.GetRepositoryURL(), name, version), "--identity", fmt.Sprintf("name=%s,version=%s", localResource.Resource.Name, localResource.Resource.Version), "--output", output, "--config", - cfgPath, + suite.ConfigPath, "--extraction-policy", resourceCMD.ExtractionPolicyDisable, }) @@ -188,13 +141,13 @@ configurations: downloadCMD.SetArgs([]string{ "download", "resource", - fmt.Sprintf("http://%s//%s:%s", registryAddress, name, version), + fmt.Sprintf("%s//%s:%s", suite.GetRepositoryURL(), name, version), "--identity", fmt.Sprintf("name=%s,version=%s", localResource.Resource.Name, localResource.Resource.Version), "--output", output, "--config", - cfgPath, + suite.ConfigPath, }) r.NoError(downloadCMD.ExecuteContext(t.Context())) diff --git a/cli/integration/suite_test.go b/cli/integration/suite_test.go new file mode 100644 index 000000000..4da8fe7f5 --- /dev/null +++ b/cli/integration/suite_test.go @@ -0,0 +1,152 @@ +package integration + +import ( + "fmt" + "net" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/log" + "github.com/testcontainers/testcontainers-go/modules/registry" + + "ocm.software/open-component-model/bindings/go/oci" + urlresolver "ocm.software/open-component-model/bindings/go/oci/resolver/url" + "ocm.software/open-component-model/cli/integration/internal" +) + +// TestSuite holds shared test infrastructure +type TestSuite struct { + RegistryAddress string + Username string + Password string + ConfigPath string + Repository *oci.Repository + once sync.Once +} + +var testSuite TestSuite + +// SetupTestSuite initializes the shared test infrastructure once +func SetupTestSuite(t *testing.T) *TestSuite { + testSuite.once.Do(func() { + setupRegistryWithoutCleanup(t) + }) + return &testSuite +} + +// setupRegistryWithoutCleanup creates a containerized registry for OCI tests but doesn't tie cleanup to individual tests +func setupRegistryWithoutCleanup(t *testing.T) { + r := require.New(t) + + t.Logf("Setting up shared test registry (no individual cleanup)") + testSuite.Username = "ocm" + + testSuite.Password = internal.GenerateRandomPassword(t, 20) + htpasswd := internal.GenerateHtpasswd(t, testSuite.Username, testSuite.Password) + testSuite.RegistryAddress = startRegistryForSharing(t, htpasswd) + host, port, err := net.SplitHostPort(testSuite.RegistryAddress) + r.NoError(err) + + cfg := fmt.Sprintf(` +type: generic.config.ocm.software/v1 +configurations: +- type: credentials.config.ocm.software + consumers: + - identity: + type: OCIRepository/v1 + hostname: %[1]q + port: %[2]q + scheme: http + credentials: + - type: Credentials/v1 + properties: + username: %[3]q + password: %[4]q +`, host, port, testSuite.Username, testSuite.Password) + + globalTempDir := os.TempDir() + testSuite.ConfigPath = filepath.Join(globalTempDir, fmt.Sprintf("ocmconfig-shared-%d.yaml", os.Getpid())) + r.NoError(os.WriteFile(testSuite.ConfigPath, []byte(cfg), os.ModePerm)) + + t.Logf("Generated shared config at: %s", testSuite.ConfigPath) + + client := internal.CreateAuthClient(testSuite.RegistryAddress, testSuite.Username, testSuite.Password) + + resolver, err := urlresolver.New( + urlresolver.WithBaseURL(testSuite.RegistryAddress), + urlresolver.WithPlainHTTP(true), + urlresolver.WithBaseClient(client), + ) + r.NoError(err) + + testSuite.Repository, err = oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(t.TempDir())) + r.NoError(err) + + t.Logf("Shared test registry setup complete: %s", testSuite.RegistryAddress) +} + +// CreateComponentConstructor creates a component constructor file for testing +func (ts *TestSuite) CreateComponentConstructor(t *testing.T, componentName, componentVersion string) string { + constructorContent := fmt.Sprintf(` +components: +- name: %s + version: %s + provider: + name: ocm.software + resources: + - name: test-resource + version: %s + type: plainText + input: + type: utf8 + text: "Hello, World from %s!" +`, componentName, componentVersion, componentVersion, componentName) + + constructorPath := filepath.Join(t.TempDir(), "constructor.yaml") + require.NoError(t, os.WriteFile(constructorPath, []byte(constructorContent), os.ModePerm)) + return constructorPath +} + +// GetRepositoryURL returns the base repository URL for the shared registry +func (ts *TestSuite) GetRepositoryURL() string { + return fmt.Sprintf("http://%s", ts.RegistryAddress) +} + +// GetRepositoryURLWithPrefix returns the repository URL with the specified type prefix +func (ts *TestSuite) GetRepositoryURLWithPrefix(prefix string) string { + return fmt.Sprintf("%s::%s", prefix, ts.GetRepositoryURL()) +} + +// startRegistryForSharing starts a registry container that will be cleaned up by testcontainers' ryuk, not individual tests +func startRegistryForSharing(t *testing.T, htpasswd string) string { + const distributionRegistryImage = "registry:3.0.0" + + t.Helper() + r := require.New(t) + + t.Logf("Launching shared test registry (%s)...", distributionRegistryImage) + registryContainer, err := registry.Run(t.Context(), distributionRegistryImage, + registry.WithHtpasswd(htpasswd), + testcontainers.WithEnv(map[string]string{ + "REGISTRY_VALIDATION_DISABLED": "true", + "REGISTRY_LOG_LEVEL": "debug", + }), + testcontainers.WithLogger(log.TestLogger(t)), + ) + r.NoError(err) + + // NOTE: Intentionally NOT calling t.Cleanup() here + // The container will be cleaned up by testcontainer when the process ends + // This prevents individual tests from terminating the shared registry + + t.Logf("Shared test registry started") + + registryAddress, err := registryContainer.HostAddress(t.Context()) + r.NoError(err) + + return registryAddress +} From ecb5e7587043753dd425ed46731fc32085ef718d Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:49:24 +0200 Subject: [PATCH 09/10] using proper TestMain for a setup sequence On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/integration/suite_test.go | 147 ++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 59 deletions(-) diff --git a/cli/integration/suite_test.go b/cli/integration/suite_test.go index 4da8fe7f5..a03301ed1 100644 --- a/cli/integration/suite_test.go +++ b/cli/integration/suite_test.go @@ -1,11 +1,11 @@ package integration import ( + "context" "fmt" "net" "os" "path/filepath" - "sync" "testing" "github.com/stretchr/testify/require" @@ -25,32 +25,75 @@ type TestSuite struct { Password string ConfigPath string Repository *oci.Repository - once sync.Once } -var testSuite TestSuite +var ( + globalTestSuite *TestSuite +) + +// TestMain sets up and tears down the test suite +func TestMain(m *testing.M) { + var exitCode int + + // Setup + globalTestSuite = &TestSuite{} + err := globalTestSuite.setup() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to setup test suite: %v\n", err) + os.Exit(1) + } + + // Run tests + exitCode = m.Run() + + // Teardown + globalTestSuite.teardown() -// SetupTestSuite initializes the shared test infrastructure once + os.Exit(exitCode) +} + +// SetupTestSuite returns the global test suite instance func SetupTestSuite(t *testing.T) *TestSuite { - testSuite.once.Do(func() { - setupRegistryWithoutCleanup(t) - }) - return &testSuite + if globalTestSuite == nil { + t.Fatal("Test suite not initialized. TestMain should handle this.") + } + return globalTestSuite } -// setupRegistryWithoutCleanup creates a containerized registry for OCI tests but doesn't tie cleanup to individual tests -func setupRegistryWithoutCleanup(t *testing.T) { - r := require.New(t) +// setup initializes the test suite +func (ts *TestSuite) setup() error { + ctx := context.Background() - t.Logf("Setting up shared test registry (no individual cleanup)") - testSuite.Username = "ocm" + ts.Username = "ocm" - testSuite.Password = internal.GenerateRandomPassword(t, 20) - htpasswd := internal.GenerateHtpasswd(t, testSuite.Username, testSuite.Password) - testSuite.RegistryAddress = startRegistryForSharing(t, htpasswd) - host, port, err := net.SplitHostPort(testSuite.RegistryAddress) - r.NoError(err) + // Generate password and credentials + ts.Password = internal.GenerateRandomPassword(&testing.T{}, 20) + htpasswd := internal.GenerateHtpasswd(&testing.T{}, ts.Username, ts.Password) + // Start registry container + registryContainer, err := registry.Run(ctx, "registry:3.0.0", + registry.WithHtpasswd(htpasswd), + testcontainers.WithEnv(map[string]string{ + "REGISTRY_VALIDATION_DISABLED": "true", + "REGISTRY_LOG_LEVEL": "debug", + }), + testcontainers.WithLogger(log.Default()), + ) + if err != nil { + return fmt.Errorf("failed to start registry: %w", err) + } + + ts.RegistryAddress, err = registryContainer.HostAddress(ctx) + if err != nil { + return fmt.Errorf("failed to get registry address: %w", err) + } + + host, port, err := net.SplitHostPort(ts.RegistryAddress) + if err != nil { + return fmt.Errorf("failed to parse registry address: %w", err) + } + + // Generate OCM config cfg := fmt.Sprintf(` type: generic.config.ocm.software/v1 configurations: @@ -66,27 +109,43 @@ configurations: properties: username: %[3]q password: %[4]q -`, host, port, testSuite.Username, testSuite.Password) +`, host, port, ts.Username, ts.Password) globalTempDir := os.TempDir() - testSuite.ConfigPath = filepath.Join(globalTempDir, fmt.Sprintf("ocmconfig-shared-%d.yaml", os.Getpid())) - r.NoError(os.WriteFile(testSuite.ConfigPath, []byte(cfg), os.ModePerm)) - - t.Logf("Generated shared config at: %s", testSuite.ConfigPath) + ts.ConfigPath = filepath.Join(globalTempDir, fmt.Sprintf("ocmconfig-suite-%d.yaml", os.Getpid())) + if err := os.WriteFile(ts.ConfigPath, []byte(cfg), os.ModePerm); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } - client := internal.CreateAuthClient(testSuite.RegistryAddress, testSuite.Username, testSuite.Password) + // Setup OCI client and repository + client := internal.CreateAuthClient(ts.RegistryAddress, ts.Username, ts.Password) resolver, err := urlresolver.New( - urlresolver.WithBaseURL(testSuite.RegistryAddress), + urlresolver.WithBaseURL(ts.RegistryAddress), urlresolver.WithPlainHTTP(true), urlresolver.WithBaseClient(client), ) - r.NoError(err) + if err != nil { + return fmt.Errorf("failed to create resolver: %w", err) + } - testSuite.Repository, err = oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(t.TempDir())) - r.NoError(err) + ts.Repository, err = oci.NewRepository(oci.WithResolver(resolver), oci.WithTempDir(os.TempDir())) + if err != nil { + return fmt.Errorf("failed to create repository: %w", err) + } - t.Logf("Shared test registry setup complete: %s", testSuite.RegistryAddress) + fmt.Printf("Shared test registry setup complete: %s\n", ts.RegistryAddress) + return nil +} + +// teardown cleans up the test suite +func (ts *TestSuite) teardown() { + if ts.ConfigPath != "" { + if err := os.Remove(ts.ConfigPath); err != nil { + fmt.Printf("Warning: failed to cleanup config file %s: %v\n", ts.ConfigPath, err) + } + } + fmt.Println("Suite teardown complete") } // CreateComponentConstructor creates a component constructor file for testing @@ -120,33 +179,3 @@ func (ts *TestSuite) GetRepositoryURL() string { func (ts *TestSuite) GetRepositoryURLWithPrefix(prefix string) string { return fmt.Sprintf("%s::%s", prefix, ts.GetRepositoryURL()) } - -// startRegistryForSharing starts a registry container that will be cleaned up by testcontainers' ryuk, not individual tests -func startRegistryForSharing(t *testing.T, htpasswd string) string { - const distributionRegistryImage = "registry:3.0.0" - - t.Helper() - r := require.New(t) - - t.Logf("Launching shared test registry (%s)...", distributionRegistryImage) - registryContainer, err := registry.Run(t.Context(), distributionRegistryImage, - registry.WithHtpasswd(htpasswd), - testcontainers.WithEnv(map[string]string{ - "REGISTRY_VALIDATION_DISABLED": "true", - "REGISTRY_LOG_LEVEL": "debug", - }), - testcontainers.WithLogger(log.TestLogger(t)), - ) - r.NoError(err) - - // NOTE: Intentionally NOT calling t.Cleanup() here - // The container will be cleaned up by testcontainer when the process ends - // This prevents individual tests from terminating the shared registry - - t.Logf("Shared test registry started") - - registryAddress, err := registryContainer.HostAddress(t.Context()) - r.NoError(err) - - return registryAddress -} From a7c9ab2e984ada59b9bb7dce8cadd52582d16d75 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:02:49 +0200 Subject: [PATCH 10/10] removed unused credential start registry helper On-behalf-of: Gergely Brautigam Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> --- cli/integration/internal/credentials.go | 30 ------------------------- 1 file changed, 30 deletions(-) diff --git a/cli/integration/internal/credentials.go b/cli/integration/internal/credentials.go index 49a601357..4ce78fbb6 100644 --- a/cli/integration/internal/credentials.go +++ b/cli/integration/internal/credentials.go @@ -8,9 +8,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/log" - "github.com/testcontainers/testcontainers-go/modules/registry" "golang.org/x/crypto/bcrypt" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/retry" @@ -54,30 +51,3 @@ func CreateAuthClient(address, username, password string) *auth.Client { }), } } - -const distributionRegistryImage = "registry:3.0.0" - -func StartDockerContainerRegistry(t *testing.T, htpasswd string) string { - t.Helper() - // Start containerized registry - t.Logf("Launching test registry (%s)...", distributionRegistryImage) - registryContainer, err := registry.Run(t.Context(), distributionRegistryImage, - registry.WithHtpasswd(htpasswd), - testcontainers.WithEnv(map[string]string{ - "REGISTRY_VALIDATION_DISABLED": "true", - "REGISTRY_LOG_LEVEL": "debug", - }), - testcontainers.WithLogger(log.TestLogger(t)), - ) - r := require.New(t) - r.NoError(err) - t.Cleanup(func() { - r.NoError(testcontainers.TerminateContainer(registryContainer)) - }) - t.Logf("Test registry started") - - registryAddress, err := registryContainer.HostAddress(t.Context()) - r.NoError(err) - - return registryAddress -}