Skip to content
29 changes: 19 additions & 10 deletions cli/cmd/add/component-version/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
Expand All @@ -28,14 +29,14 @@ 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"
)

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"
Expand Down Expand Up @@ -106,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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not actually a repository specification, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a repository url I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can be sort a specification because it's no longer "just a path". What could we call this?

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")
Expand Down Expand Up @@ -220,19 +221,27 @@ 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)
}
var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite
if !repoRef.Exists() {
accessMode += "|" + ctfv1.AccessModeCreate

typed, err := compref.ParseRepository(repositorySpec)
if err != nil {
return nil, fmt.Errorf("failed to parse repository: %w", err)
}
repoSpec := ctfv1.Repository{
Path: repoRef.String(),
AccessMode: accessMode,

// Handle CTF-specific access mode configuration
if ctfRepo, ok := typed.(*ctfv1.Repository); ok {
var accessMode ctfv1.AccessMode = ctfv1.AccessModeReadWrite
// For CTF repositories, check if path exists to determine access mode
if _, err := os.Stat(ctfRepo.Path); os.IsNotExist(err) {
accessMode += "|" + ctfv1.AccessModeCreate
}
ctfRepo.AccessMode = accessMode
}
return &repoSpec, nil

return typed, nil
}

func GetComponentConstructor(file *file.Flag) (*constructorruntime.ComponentConstructor, error) {
Expand Down
2 changes: 1 addition & 1 deletion cli/docs/reference/ocm_add_component-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```

Expand Down
160 changes: 160 additions & 0 deletions cli/integration/add_component_version_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package integration

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"ocm.software/open-component-model/cli/cmd"
)

func Test_Integration_AddComponentVersion_OCIRepository(t *testing.T) {
suite := SetupTestSuite(t)

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 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", suite.GetRepositoryURL(),
"--constructor", constructorPath,
"--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 := 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(componentVersion, 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 using suite helper
constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion)

// Test with explicit oci:: prefix
addCMD := cmd.New()
addCMD.SetArgs([]string{
"add",
"component-version",
"--repository", suite.GetRepositoryURLWithPrefix("oci"),
"--constructor", constructorPath,
"--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 := 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 HTTP URL format", func(t *testing.T) {
r := require.New(t)

componentName := "ocm.software/http-component"
componentVersion := "v3.0.0"

// Create constructor file using suite helper
constructorPath := suite.CreateComponentConstructor(t, componentName, componentVersion)

// Test with HTTP URL format
addCMD := cmd.New()
addCMD.SetArgs([]string{
"add",
"component-version",
"--repository", suite.GetRepositoryURL(),
"--constructor", constructorPath,
"--config", suite.ConfigPath,
})

r.NoError(addCMD.ExecuteContext(t.Context()), "add component-version should succeed with HTTP URL format")

// Verify the component version was added
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.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 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")
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 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")
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")
})
}
67 changes: 10 additions & 57 deletions cli/integration/download_resource_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
Expand All @@ -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"
Expand All @@ -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)
Expand All @@ -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()

Expand All @@ -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()))

Expand Down Expand Up @@ -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)
Expand All @@ -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,
})
Expand All @@ -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()))

Expand Down
Loading
Loading