diff --git a/.github/workflows/push_ocm.yaml b/.github/workflows/push_ocm.yaml
index f33d405f9..582987f17 100644
--- a/.github/workflows/push_ocm.yaml
+++ b/.github/workflows/push_ocm.yaml
@@ -1,7 +1,7 @@
name: publish as latest
on:
# publish on pushes to the main branch (image tagged as "latest")
- # https://github.com/open-component-model/ocm/pkgs/container/ocm
+ # https://ocm.software/ocm/pkgs/container/ocm
push:
branches:
- main
diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml
index 61b592d2d..e97af0a8b 100644
--- a/.github/workflows/release-drafter.yaml
+++ b/.github/workflows/release-drafter.yaml
@@ -28,7 +28,7 @@ jobs:
- name: Set Version
run: |
- RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)
+ RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)
echo "release version is $RELEASE_VERSION"
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 2c4af9e9b..f9ae26192 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -37,14 +37,14 @@ jobs:
run: |
echo "Release Job Arguments"
if ${{ github.event.inputs.release_candidate }}; then
- v="v$(go run $GITHUB_WORKSPACE/pkg/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})"
+ v="v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})"
if [ -n "${{ github.event.inputs.prerelease }}" ]; then
echo "Candidate: $v"
else
echo "Candidate: $v (taken from source)"
fi
else
- v="v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)"
+ v="v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)"
echo "Final Release: $v"
if ${{ github.event.inputs.create_branch }}; then
echo "with release branch creation"
@@ -55,13 +55,13 @@ jobs:
- name: Set Base Version
run: |
- BASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)
+ BASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)
echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV
- name: Set Pre-Release Version
if: inputs.release_candidate == true
run: |
- RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})
+ RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
- name: Set Version
@@ -164,13 +164,13 @@ jobs:
- name: Set Base Version
run: |
- BASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)
+ BASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)
echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV
- name: Set Pre-Release Version
if: inputs.release_candidate == true
run: |
- RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})
+ RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
echo "release name is $RELEASE_VERSION"
@@ -247,7 +247,7 @@ jobs:
run: |
n="releases/${{env.RELEASE_VERSION}}"
git checkout -b "$n"
- v="$(go run ./pkg/version/generate bump-patch)"
+ v="$(go run ./api/version/generate bump-patch)"
echo "$v" > VERSION
git add VERSION
git commit -m "Prepare Development of v$v"
@@ -258,7 +258,7 @@ jobs:
run: |
set -e
git checkout ${GITHUB_REF#refs/heads/}
- v="$(go run ./pkg/version/generate bump-version)"
+ v="$(go run ./api/version/generate bump-version)"
echo "$v" > VERSION
git add VERSION
git commit -m "Update version file to $v"
diff --git a/.github/workflows/releasenotes.yaml b/.github/workflows/releasenotes.yaml
index e3ebcaa27..d20a5e43b 100644
--- a/.github/workflows/releasenotes.yaml
+++ b/.github/workflows/releasenotes.yaml
@@ -29,7 +29,7 @@ jobs:
- name: Set Version
run: |
- RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)
+ RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)
echo "release version is $RELEASE_VERSION"
echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
@@ -48,7 +48,7 @@ jobs:
run: |
set -e
echo "Release Notes:\n $RELEASE_NOTES'"
- v="v$(go run $GITHUB_WORKSPACE/pkg/version/generate print-version)"
+ v="v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)"
f="docs/releasenotes/$v.md"
echo "$RELEASE_NOTES" > "$f"
git add "$f"
diff --git a/.golangci.yaml b/.golangci.yaml
index e8aa23bd0..a2d6efa90 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -98,7 +98,7 @@ linters-settings:
- blank
- dot
- default
- - prefix(github.com/open-component-model/ocm)
+ - prefix(ocm.software/ocm)
custom-order: true
funlen:
lines: 110
@@ -129,7 +129,7 @@ issues:
exclude-dirs:
- "hack"
# External code from containerd/containerd
- - "pkg/docker"
+ - "api/tech/docker"
exclude:
- composites
exclude-rules:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 52c4f8c76..4253d4e1c 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -9,7 +9,7 @@ builds:
binary: ocm
main: ./cmds/ocm/main.go
ldflags:
- - -s -w -X github.com/open-component-model/ocm/pkg/version.gitVersion={{.Version}} -X github.com/open-component-model/ocm/pkg/version.gitCommit={{.Commit}} -X github.com/open-component-model/ocm/pkg/version.buildDate={{.CommitDate}}
+ - -s -w -X ocm.software/ocm/api/version.gitVersion={{.Version}} -X ocm.software/ocm/api/version.gitCommit={{.Commit}} -X ocm.software/ocm/api/version.buildDate={{.CommitDate}}
env:
- CGO_ENABLED=0
id: linux
diff --git a/.reuse/dep5 b/.reuse/dep5
index e75df02db..55264e46d 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -1,7 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ocm
Upstream-Contact: ospo@sap.com
-Source: https://github.com/open-component-model/ocm
+Source: https://ocm.software/ocm
Disclaimer: The code in this project may include calls to APIs ("API Calls") of
SAP or third-party products or services developed outside of this project
("External Products").
@@ -28,6 +28,6 @@ Files: **
Copyright: 2024 SAP SE or an SAP affiliate company and Open Component Model contributors
License: Apache-2.0
-Files: pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go
+Files: api/ocm/extensions/blobhandler/handlers/generic/npm/publish.go
Copyright: Copyright 2021 - cloverstd
License: MIT
diff --git a/Dockerfile b/Dockerfile
index 45a407ccf..2364fc51b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,10 +14,10 @@ RUN --mount=type=cache,target=/root/.cache/go-build go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
- export VERSION=$(go run pkg/version/generate/release_generate.go print-rc-version) && \
+ export VERSION=$(go run api/version/generate/release_generate.go print-rc-version) && \
export NOW=$(date -u +%FT%T%z) && \
go build -trimpath -ldflags \
- "-s -w -X github.com/open-component-model/ocm/pkg/version.gitVersion=$VERSION -X github.com/open-component-model/ocm/pkg/version.buildDate=$NOW" \
+ "-s -w -X ocm.software/ocm/api/version.gitVersion=$VERSION -X ocm.software/ocm/api/version.buildDate=$NOW" \
-o /bin/ocm ./cmds/ocm/main.go
FROM alpine:${ALPINE_VERSION}
diff --git a/Makefile b/Makefile
index e35074471..80eaadd33 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ NAME := ocm
REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
GITHUBORG ?= open-component-model
OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm
-VERSION := $(shell go run pkg/version/generate/release_generate.go print-rc-version $(CANDIDATE))
+VERSION := $(shell go run api/version/generate/release_generate.go print-rc-version $(CANDIDATE))
EFFECTIVE_VERSION := $(VERSION)+$(shell git rev-parse HEAD)
GIT_TREE_STATE := $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty)
COMMIT := $(shell git rev-parse --verify HEAD)
@@ -23,17 +23,17 @@ GOPATH := $(shell go env GOPATH)
NOW := $(shell date -u +%FT%T%z)
BUILD_FLAGS := "-s -w \
- -X github.com/open-component-model/ocm/pkg/version.gitVersion=$(EFFECTIVE_VERSION) \
- -X github.com/open-component-model/ocm/pkg/version.gitTreeState=$(GIT_TREE_STATE) \
- -X github.com/open-component-model/ocm/pkg/version.gitCommit=$(COMMIT) \
- -X github.com/open-component-model/ocm/pkg/version.buildDate=$(NOW)"
+ -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \
+ -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \
+ -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \
+ -X ocm.software/ocm/api/version.buildDate=$(NOW)"
COMPONENTS ?= ocmcli helminstaller demoplugin ecrplugin helmdemo subchartsdemo
.PHONY: build
build: ${SOURCES}
mkdir -p bin
- go build ./pkg/...
+ go build ./api/...
go build ./examples/...
CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/ocm ./cmds/ocm
CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/helminstaller ./cmds/helminstaller
@@ -55,7 +55,7 @@ install-requirements:
.PHONY: prepare
prepare: generate format generate-deepcopy build test check
-EFFECTIVE_DIRECTORIES := $(REPO_ROOT)/cmds/ocm/... $(REPO_ROOT)/cmds/helminstaller/... $(REPO_ROOT)/cmds/ecrplugin/... $(REPO_ROOT)/cmds/demoplugin/... $(REPO_ROOT)/cmds/cliplugin/... $(REPO_ROOT)/examples/... $(REPO_ROOT)/cmds/subcmdplugin/... $(REPO_ROOT)/pkg/...
+EFFECTIVE_DIRECTORIES := $(REPO_ROOT)/cmds/ocm/... $(REPO_ROOT)/cmds/helminstaller/... $(REPO_ROOT)/cmds/ecrplugin/... $(REPO_ROOT)/cmds/demoplugin/... $(REPO_ROOT)/cmds/cliplugin/... $(REPO_ROOT)/examples/... $(REPO_ROOT)/cmds/subcmdplugin/... $(REPO_ROOT)/api/...
.PHONY: format
format:
@@ -89,7 +89,7 @@ generate:
.PHONY: generate-deepcopy
generate-deepcopy: controller-gen
- $(CONTROLLER_GEN) object paths=./pkg/contexts/ocm/compdesc/versions/... paths=./pkg/contexts/ocm/compdesc/meta/...
+ $(CONTROLLER_GEN) object paths=./api/ocm/compdesc/versions/... paths=./api/ocm/compdesc/meta/...
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
diff --git a/README.md b/README.md
index cb7225c81..e50c42471 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# Open Component Model
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7156/badge)](https://bestpractices.coreinfrastructure.org/projects/7156)
-[![REUSE status](https://api.reuse.software/badge/github.com/open-component-model/ocm)](https://api.reuse.software/info/github.com/open-component-model/ocm)
-[![OCM Integration Tests](https://github.com/open-component-model/ocm-integrationtest/actions/workflows/integrationtest.yaml/badge.svg?branch=main)](https://open-component-model.github.io/ocm-integrationtest/report.html)
-[![Go Report Card](https://goreportcard.com/badge/github.com/open-component-model/ocm)](https://goreportcard.com/report/github.com/open-component-model/ocm)
+[![REUSE status](https://api.reuse.software/badge/ocm.software/ocm)](https://api.reuse.software/info/ocm.software/ocm)
+[![OCM Integration Tests](https://ocm.software/ocm-integrationtest/actions/workflows/integrationtest.yaml/badge.svg?branch=main)](https://open-component-model.github.io/ocm-integrationtest/report.html)
+[![Go Report Card](https://goreportcard.com/badge/ocm.software/ocm)](https://goreportcard.com/report/ocm.software/ocm)
The Open Component Model (OCM) is an open standard to describe software bills of delivery (SBOD). OCM is a technology-agnostic and machine-readable format focused on the software artifacts that must be delivered for software products.
@@ -11,14 +11,14 @@ Check out the [the main OCM project web page](https://ocm.software) to find out
## OCM Specifications
-OCM describes delivery [artifacts](https://github.com/open-component-model/ocm-spec/tree/main/doc/01-model/02-elements-toplevel.md#artifacts-resources-and-sources) that can be accessed from many types of [component repositories](https://github.com/open-component-model/ocm-spec/tree/main/doc/01-model/01-model.md#component-repositories). It defines a set of semantic, formatting, and other types of specifications that can be found in the [`ocm-spec` repository](https://github.com/open-component-model/ocm-spec). Start learning about the core concepts of OCM elements [here](https://github.com/open-component-model/ocm-spec/tree/main/doc/01-model/02-elements-toplevel.md#model-elements).
+OCM describes delivery [artifacts](https://ocm.software/ocm-spec/tree/main/doc/01-model/02-elements-toplevel.md#artifacts-resources-and-sources) that can be accessed from many types of [component repositories](https://ocm.software/ocm-spec/tree/main/doc/01-model/01-model.md#component-repositories). It defines a set of semantic, formatting, and other types of specifications that can be found in the [`ocm-spec` repository](https://ocm.software/ocm-spec). Start learning about the core concepts of OCM elements [here](https://ocm.software/ocm-spec/tree/main/doc/01-model/02-elements-toplevel.md#model-elements).
## OCM Library
This project provides a Go library containing an API for interacting with the
-[Open Component Model (OCM)](https://github.com/open-component-model/ocm-spec) elements and mechanisms.
+[Open Component Model (OCM)](https://ocm.software/ocm-spec) elements and mechanisms.
-The library currently supports the following [repository mappings](https://github.com/open-component-model/ocm-spec/tree/main/doc/03-persistence/02-mappings.md#mappings-for-ocm-persistence):
+The library currently supports the following [repository mappings](https://ocm.software/ocm-spec/tree/main/doc/03-persistence/02-mappings.md#mappings-for-ocm-persistence):
- **OCI**: Use the repository prefix path of an OCI repository to implement an OCM
repository.
@@ -40,12 +40,12 @@ Additionally, OCM provides a generic solution for how to:
The [`ocm` CLI](docs/reference/ocm.md) may also be used to interact with OCM mechanisms. It makes it easy to create component versions and embed them in build processes.
-The `ocm` CLI documentation can be found [here](<(https://github.com/open-component-model/ocm/blob/main/docs/reference/ocm.md)>).
+The `ocm` CLI documentation can be found [here](<(https://ocm.software/ocm/blob/main/docs/reference/ocm.md)>).
-The code for the CLI can be found in [packageĀ `cmds/ocm`](https://github.com/open-component-model/ocm/blob/main/cmds/ocm).
+The code for the CLI can be found in [packageĀ `cmds/ocm`](https://ocm.software/ocm/blob/main/cmds/ocm).
The OCI and OCM support can be found in packages
-[`pkg/contexts/oci`](pkg/contexts/oci) and [`pkg/contexts/ocm`](pkg/contexts/ocm).
+[`pkg/contexts/oci`](pkg/contexts/oci) and [`api/ocm`](api/ocm).
## Installation
@@ -56,7 +56,7 @@ Install the latest release from any of
- [AUR](https://aur.archlinux.org/packages/ocm-cli)
- [Docker](https://www.docker.com/)
- [Podman](https://podman.io/)
-- [GitHub Releases](https://github.com/open-component-model/ocm/releases)
+- [GitHub Releases](https://ocm.software/ocm/releases)
### Bash
@@ -140,9 +140,9 @@ podman build -t ocm --build-arg GO_VERSION=1.22 --build-arg ALPINE_VERSION=3.19
## Examples
-An example of how to use the `ocm` CLI in a Makefile can be found in [`examples/make`](https://github.com/open-component-model/ocm/blob/main/examples/make/Makefile).
+An example of how to use the `ocm` CLI in a Makefile can be found in [`examples/make`](https://ocm.software/ocm/blob/main/examples/make/Makefile).
-More comprehensive examples can be taken from the [`components`](https://github.com/open-component-model/ocm/tree/main/components) contained in this repository. [Here](components/helmdemo/README.md) a complete component build including a multi-arch image is done and finally packaged into a CTF archive which can be tranported into an OCI repository. See the readme files for details.
+More comprehensive examples can be taken from the [`components`](https://ocm.software/ocm/tree/main/components) contained in this repository. [Here](components/helmdemo/README.md) a complete component build including a multi-arch image is done and finally packaged into a CTF archive which can be tranported into an OCI repository. See the readme files for details.
## Contributing
@@ -154,4 +154,4 @@ OCM follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/m
Copyright 2024 SAP SE or an SAP affiliate company and Open Component Model contributors.
Please see our [LICENSE](LICENSE) for copyright and license information.
-Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/open-component-model/ocm).
+Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/ocm.software/ocm).
diff --git a/api/cli/builder.go b/api/cli/builder.go
new file mode 100644
index 000000000..5f20b6d03
--- /dev/null
+++ b/api/cli/builder.go
@@ -0,0 +1,44 @@
+package clictx
+
+import (
+ "context"
+ "io"
+
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/cli/internal"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm"
+)
+
+func WithContext(ctx context.Context) internal.Builder {
+ return internal.Builder{}.WithContext(ctx)
+}
+
+func WithSharedAttributes(ctx datacontext.AttributesContext) internal.Builder {
+ return internal.Builder{}.WithSharedAttributes(ctx)
+}
+
+func WithOCM(ctx ocm.Context) internal.Builder {
+ return internal.Builder{}.WithOCM(ctx)
+}
+
+func WithFileSystem(fs vfs.FileSystem) internal.Builder {
+ return internal.Builder{}.WithFileSystem(fs)
+}
+
+func WithOutput(w io.Writer) internal.Builder {
+ return internal.Builder{}.WithOutput(w)
+}
+
+func WithErrorOutput(w io.Writer) internal.Builder {
+ return internal.Builder{}.WithErrorOutput(w)
+}
+
+func WithInput(r io.Reader) internal.Builder {
+ return internal.Builder{}.WithInput(r)
+}
+
+func New(mode ...datacontext.BuilderMode) internal.Context {
+ return internal.Builder{}.New(mode...)
+}
diff --git a/api/cli/config/config_test.go b/api/cli/config/config_test.go
new file mode 100644
index 000000000..ff7536f5a
--- /dev/null
+++ b/api/cli/config/config_test.go
@@ -0,0 +1,61 @@
+package config_test
+
+import (
+ "encoding/json"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ clictx "ocm.software/ocm/api/cli"
+ "ocm.software/ocm/api/cli/config"
+ "ocm.software/ocm/api/oci/extensions/repositories/ocireg"
+ ocmocireg "ocm.software/ocm/api/ocm/extensions/repositories/ocireg"
+)
+
+var DefaultContext = clictx.New()
+
+func normalize(i interface{}) ([]byte, error) {
+ data, err := json.Marshal(i)
+ if err != nil {
+ return nil, err
+ }
+ var generic map[string]interface{}
+ err = json.Unmarshal(data, &generic)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(generic)
+}
+
+var _ = Describe("command config", func() {
+ ocispec := ocireg.NewRepositorySpec("ghcr.io")
+
+ ocidata, err := normalize(ocispec)
+ Expect(err).To(Succeed())
+
+ ocmspec := ocmocireg.NewRepositorySpec("gcr.io", nil)
+ ocmdata, err := normalize(ocmspec)
+ Expect(err).To(Succeed())
+
+ specdata := "{\"ociRepositories\":{\"oci\":" + string(ocidata) + "},\"ocmRepositories\":{\"ocm\":" + string(ocmdata) + "},\"type\":\"" + config.OCMCmdConfigType + "\"}"
+
+ Context("serialize", func() {
+ It("serializes config", func() {
+ cfg := config.New()
+ err := cfg.AddOCIRepository("oci", ocispec)
+ Expect(err).To(Succeed())
+ err = cfg.AddOCMRepository("ocm", ocmspec)
+ Expect(err).To(Succeed())
+
+ data, err := normalize(cfg)
+
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(specdata)))
+
+ cfg2 := config.New()
+ err = json.Unmarshal(data, cfg2)
+ Expect(err).To(Succeed())
+ Expect(cfg2).To(Equal(cfg))
+ })
+ })
+})
diff --git a/pkg/contexts/clictx/config/suite_test.go b/api/cli/config/suite_test.go
similarity index 100%
rename from pkg/contexts/clictx/config/suite_test.go
rename to api/cli/config/suite_test.go
diff --git a/api/cli/config/type.go b/api/cli/config/type.go
new file mode 100644
index 000000000..84ef23212
--- /dev/null
+++ b/api/cli/config/type.go
@@ -0,0 +1,100 @@
+package config
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/cli/internal"
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/config/cpi"
+ ocicpi "ocm.software/ocm/api/oci/cpi"
+ ocmcpi "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ OCMCmdConfigType = "ocm.cmd" + cpi.OCM_CONFIG_TYPE_SUFFIX
+ OCMCmdConfigTypeV1 = OCMCmdConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterConfigType(cpi.NewConfigType[*Config](OCMCmdConfigType, usage))
+ cpi.RegisterConfigType(cpi.NewConfigType[*Config](OCMCmdConfigTypeV1, usage))
+}
+
+// Config describes a memory based repository interface.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ OCMRepositories map[string]*ocmcpi.GenericRepositorySpec `json:"ocmRepositories,omitempty"`
+ OCIRepositories map[string]*ocicpi.GenericRepositorySpec `json:"ociRepositories,omitempty"`
+}
+
+// New creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(OCMCmdConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return OCMCmdConfigType
+}
+
+func (a *Config) AddOCIRepository(name string, spec ocicpi.RepositorySpec) error {
+ g, err := ocicpi.ToGenericRepositorySpec(spec)
+ if err != nil {
+ return fmt.Errorf("unable to convert oci repository spec to generic spec: %w", err)
+ }
+
+ if a.OCIRepositories == nil {
+ a.OCIRepositories = map[string]*ocicpi.GenericRepositorySpec{}
+ }
+
+ a.OCIRepositories[name] = g
+
+ return nil
+}
+
+func (a *Config) AddOCMRepository(name string, spec ocmcpi.RepositorySpec) error {
+ g, err := ocmcpi.ToGenericRepositorySpec(spec)
+ if err != nil {
+ return fmt.Errorf("unable to convert ocm repository spec to generic spec: %w", err)
+ }
+
+ if a.OCMRepositories == nil {
+ a.OCMRepositories = map[string]*ocmcpi.GenericRepositorySpec{}
+ }
+
+ a.OCMRepositories[name] = g
+
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx config.Context, target interface{}) error {
+ t, ok := target.(internal.Context)
+ if !ok {
+ return config.ErrNoContext(OCMCmdConfigType)
+ }
+ for n, s := range a.OCIRepositories {
+ t.OCI().Context().SetAlias(n, s)
+ }
+ for n, s := range a.OCMRepositories {
+ t.OCM().Context().SetAlias(n, s)
+ }
+ return nil
+}
+
+const usage = `
+The config type ` + OCMCmdConfigType + `
can be used to
+configure predefined aliases for dedicated OCM repositories and
+OCI registries.
+
+
+ type: ` + OCMCmdConfigType + ` + ocmRepositories: + <name>: <specification of OCM repository> + ... + ociRepositories: + <name>: <specification of OCI registry> + ... ++` diff --git a/api/cli/interface.go b/api/cli/interface.go new file mode 100644 index 000000000..5d216fb3a --- /dev/null +++ b/api/cli/interface.go @@ -0,0 +1,15 @@ +package clictx + +import ( + "ocm.software/ocm/api/cli/internal" +) + +type ( + Context = internal.Context + OCI = internal.OCI + OCM = internal.OCM +) + +func DefaultContext() Context { + return internal.DefaultContext +} diff --git a/api/cli/internal/builder.go b/api/cli/internal/builder.go new file mode 100644 index 000000000..fb980e003 --- /dev/null +++ b/api/cli/internal/builder.go @@ -0,0 +1,84 @@ +package internal + +import ( + "context" + "io" + + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/utils/out" +) + +type Builder struct { + ctx context.Context + shared datacontext.AttributesContext + ocm ocm.Context + out out.Context + filesystem vfs.FileSystem +} + +func (b *Builder) getContext() context.Context { + if b.ctx == nil { + return context.Background() + } + return b.ctx +} + +func (b Builder) WithContext(ctx context.Context) Builder { + b.ctx = ctx + return b +} + +func (b Builder) WithFileSystem(fs vfs.FileSystem) Builder { + b.filesystem = fs + return b +} + +func (b Builder) WithSharedAttributes(ctx datacontext.AttributesContext) Builder { + b.shared = ctx + return b +} + +func (b Builder) WithOCM(ctx ocm.Context) Builder { + b.ocm = ctx + return b +} + +func (b Builder) WithOutput(w io.Writer) Builder { + b.out = out.WithOutput(b.out, w) + return b +} + +func (b Builder) WithErrorOutput(w io.Writer) Builder { + b.out = out.WithErrorOutput(b.out, w) + return b +} + +func (b Builder) WithInput(r io.Reader) Builder { + b.out = out.WithInput(b.out, r) + return b +} + +func (b Builder) Bound() (Context, context.Context) { + c := b.New() + return c, context.WithValue(b.getContext(), key, c) +} + +func (b Builder) New(m ...datacontext.BuilderMode) Context { + mode := datacontext.Mode(m...) + ctx := b.getContext() + + if b.ocm == nil { + var ok bool + b.ocm, ok = ocm.DefinedForContext(ctx) + if !ok && mode != datacontext.MODE_SHARED { + b.ocm = ocm.New(mode) + } + } + if b.shared == nil { + b.shared = b.ocm.AttributesContext() + } + return datacontext.SetupContext(mode, newContext(b.shared, b.ocm, out.NewFor(b.out), b.filesystem, b.shared)) +} diff --git a/api/cli/internal/context.go b/api/cli/internal/context.go new file mode 100644 index 000000000..d5314bd32 --- /dev/null +++ b/api/cli/internal/context.go @@ -0,0 +1,313 @@ +package internal + +import ( + "context" + "io" + "reflect" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/config" + cfgcpi "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/oci" + ctfoci "ocm.software/ocm/api/oci/extensions/repositories/ctf" + "ocm.software/ocm/api/ocm" + ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/out" +) + +const CONTEXT_TYPE = "ocm.cmd" + datacontext.OCM_CONTEXT_SUFFIX + +type OCI interface { + Context() oci.Context + OpenCTF(path string) (oci.Repository, error) +} + +type OCM interface { + Context() ocm.Context + OpenCTF(path string) (ocm.Repository, error) +} + +type FileSystem struct { + vfs.FileSystem +} + +func (f *FileSystem) ApplyOption(options accessio.Options) error { + options.SetPathFileSystem(f.FileSystem) + return nil +} + +type ContextProvider interface { + CLIContext() Context +} + +type Context interface { + datacontext.Context + ContextProvider + datacontext.ContextProvider + config.ContextProvider + credentials.ContextProvider + oci.ContextProvider + ocm.ContextProvider + + FileSystem() *FileSystem + + OCI() OCI + OCM() OCM + + ApplyOption(options accessio.Options) error + + out.Context + WithStdIO(r io.Reader, o io.Writer, e io.Writer) Context +} + +var key = reflect.TypeOf(_context{}) + +// DefaultContext is the default context initialized by init functions. +var DefaultContext = Builder{}.New(datacontext.MODE_SHARED) + +// ForContext returns the Context to use for context.Context. +// This is either an explicit context or the default context. +// The returned context incorporates the given context. +func ForContext(ctx context.Context) Context { + c, _ := datacontext.ForContextByKey(ctx, key, DefaultContext) + return c.(Context) +} + +func DefinedForContext(ctx context.Context) (Context, bool) { + c, ok := datacontext.ForContextByKey(ctx, key, DefaultContext) + if c != nil { + return c.(Context), ok + } + return nil, ok +} + +//////////////////////////////////////////////////////////////////////////////// + +type _InternalContext = datacontext.InternalContext + +type _context struct { + _InternalContext + updater cfgcpi.Updater + + sharedAttributes datacontext.AttributesContext + + credentials credentials.Context + oci *_oci + ocm *_ocm + + out out.Context +} + +var ( + _ Context = (*_context)(nil) + _ datacontext.ViewCreator[Context] = (*_context)(nil) +) + +// gcWrapper is used as garbage collectable +// wrapper for a context implementation +// to establish a runtime finalizer. +type gcWrapper struct { + datacontext.GCWrapper + *_context +} + +func newView(c *_context, ref ...bool) Context { + if general.Optional(ref...) { + return datacontext.FinalizedContext[gcWrapper](c) + } + return c +} + +func (w *gcWrapper) SetContext(c *_context) { + w._context = c +} + +func newContext(shared datacontext.AttributesContext, ocmctx ocm.Context, outctx out.Context, fs vfs.FileSystem, delegates datacontext.Delegates) Context { + if outctx == nil { + outctx = out.New() + } + if shared == nil { + shared = ocmctx.AttributesContext() + } + c := &_context{ + sharedAttributes: datacontext.PersistentContextRef(shared), + credentials: datacontext.PersistentContextRef(ocmctx.CredentialsContext()), + out: outctx, + } + c._InternalContext = datacontext.NewContextBase(c, CONTEXT_TYPE, key, ocmctx.GetAttributes(), delegates) + c.updater = cfgcpi.NewUpdater(datacontext.PersistentContextRef(ocmctx.CredentialsContext().ConfigContext()), c) + ocmctx = datacontext.PersistentContextRef(ocmctx) + c.oci = newOCI(c, ocmctx) + c.ocm = newOCM(c, ocmctx) + if fs != nil { + vfsattr.Set(c.AttributesContext(), fs) + } + return newView(c, true) +} + +func (c *_context) CreateView() Context { + return newView(c, true) +} + +func (c *_context) CLIContext() Context { + return newView(c) +} + +func (c *_context) Update() error { + return c.updater.Update() +} + +func (c *_context) AttributesContext() datacontext.AttributesContext { + return c.sharedAttributes +} + +func (c *_context) ConfigContext() config.Context { + return c.updater.GetContext() +} + +func (c *_context) CredentialsContext() credentials.Context { + return c.credentials +} + +func (c *_context) OCIContext() oci.Context { + return c.oci.Context() +} + +func (c *_context) OCMContext() ocm.Context { + return c.ocm.Context() +} + +func (c *_context) FileSystem() *FileSystem { + return &FileSystem{vfsattr.Get(c.CLIContext())} +} + +func (c *_context) OCI() OCI { + return c.oci +} + +func (c *_context) OCM() OCM { + return c.ocm +} + +func (c *_context) ApplyOption(options accessio.Options) error { + options.SetPathFileSystem(c.FileSystem()) + return nil +} + +func (c *_context) StdOut() io.Writer { + return c.out.StdOut() +} + +func (c *_context) StdErr() io.Writer { + return c.out.StdErr() +} + +func (c *_context) StdIn() io.Reader { + return c.out.StdIn() +} + +func (c *_context) WithStdIO(r io.Reader, o io.Writer, e io.Writer) Context { + return &_view{ + Context: c.CLIContext(), + out: out.NewFor(out.WithStdIO(c.out, r, o, e)), + } +} + +//////////////////////////////////////////////////////////////////////////////// + +type _view struct { + Context + out out.Context +} + +func (c *_view) StdOut() io.Writer { + return c.out.StdOut() +} + +func (c *_view) StdErr() io.Writer { + return c.out.StdErr() +} + +func (c *_view) StdIn() io.Reader { + return c.out.StdIn() +} + +func (c *_view) WithStdIO(r io.Reader, o io.Writer, e io.Writer) Context { + return &_view{ + Context: c.CLIContext(), + out: out.NewFor(out.WithStdIO(c.out, r, o, e)), + } +} + +//////////////////////////////////////////////////////////////////////////////// +// the coding for _oci and _ocm is identical except the package used: +// _oci uses oci and ctfoci +// _ocm uses ocm and ctfocm + +type _oci struct { + cli *_context + ctx oci.Context + repos map[string]oci.RepositorySpec +} + +func newOCI(ctx *_context, ocmctx ocm.Context) *_oci { + return &_oci{ + cli: ctx, + ctx: ocmctx.OCIContext(), + repos: map[string]oci.RepositorySpec{}, + } +} + +func (c *_oci) Context() oci.Context { + return c.ctx +} + +func (c *_oci) OpenCTF(path string) (oci.Repository, error) { + ok, err := vfs.Exists(c.cli.FileSystem(), path) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.ErrNotFound("file", path) + } + return ctfoci.Open(c.ctx, accessobj.ACC_WRITABLE, path, 0, accessio.PathFileSystem(c.cli.FileSystem())) +} + +//////////////////////////////////////////////////////////////////////////////// + +type _ocm struct { + cli *_context + ctx ocm.Context + repos map[string]ocm.RepositorySpec +} + +func newOCM(ctx *_context, ocmctx ocm.Context) *_ocm { + return &_ocm{ + cli: ctx, + ctx: ocmctx, + repos: map[string]ocm.RepositorySpec{}, + } +} + +func (c *_ocm) Context() ocm.Context { + return c.ctx +} + +func (c *_ocm) OpenCTF(path string) (ocm.Repository, error) { + ok, err := vfs.Exists(c.cli.FileSystem(), path) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.ErrNotFound("file", path) + } + return ctfocm.Open(c.ctx, accessobj.ACC_WRITABLE, path, 0, c.cli.FileSystem()) +} diff --git a/pkg/contexts/config/README.md b/api/config/README.md similarity index 100% rename from pkg/contexts/config/README.md rename to api/config/README.md diff --git a/api/config/builder.go b/api/config/builder.go new file mode 100644 index 000000000..ac603d212 --- /dev/null +++ b/api/config/builder.go @@ -0,0 +1,24 @@ +package config + +import ( + "context" + + "ocm.software/ocm/api/config/internal" + "ocm.software/ocm/api/datacontext" +) + +func WithContext(ctx context.Context) internal.Builder { + return internal.Builder{}.WithContext(ctx) +} + +func WithSharedAttributes(ctx datacontext.AttributesContext) internal.Builder { + return internal.Builder{}.WithSharedAttributes(ctx) +} + +func WithConfigTypeScheme(scheme ConfigTypeScheme) internal.Builder { + return internal.Builder{}.WithConfigTypeScheme(scheme) +} + +func New(mode ...datacontext.BuilderMode) Context { + return internal.Builder{}.New(mode...) +} diff --git a/api/config/configutils/configure.go b/api/config/configutils/configure.go new file mode 100644 index 000000000..44238cfc8 --- /dev/null +++ b/api/config/configutils/configure.go @@ -0,0 +1,20 @@ +package configutils + +import ( + _ "ocm.software/ocm/api/datacontext/config" + + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/config" + utils "ocm.software/ocm/api/ocm/ocmutils" +) + +func Configure(path string, fss ...vfs.FileSystem) error { + _, err := utils.Configure(config.DefaultContext(), path, fss...) + return err +} + +func ConfigureContext(ctxp config.ContextProvider, path string, fss ...vfs.FileSystem) error { + _, err := utils.Configure(ctxp, path, fss...) + return err +} diff --git a/api/config/context_test.go b/api/config/context_test.go new file mode 100644 index 000000000..6f2ba63ce --- /dev/null +++ b/api/config/context_test.go @@ -0,0 +1,89 @@ +package config_test + +import ( + "encoding/json" + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/datacontext" +) + +var _ = Describe("config handling", func() { + var scheme config.ConfigTypeScheme + var cfgctx config.Context + + BeforeEach(func() { + scheme = config.NewConfigTypeScheme() + cfgctx = config.WithConfigTypeScheme(scheme).New() + Expect(cfgctx.AttributesContext().GetId()).NotTo(BeIdenticalTo(datacontext.DefaultContext.GetId())) + }) + + It("can deserialize unknown", func() { + cfg := NewConfig("a", "b") + data, err := json.Marshal(cfg) + Expect(err).To(Succeed()) + + result, err := cfgctx.GetConfigForData(data, nil) + Expect(err).To(Succeed()) + Expect(config.IsGeneric(result)).To(BeTrue()) + }) + + It("can deserialize known", func() { + RegisterAt(scheme) + + cfg := NewConfig("a", "b") + data, err := json.Marshal(cfg) + Expect(err).To(Succeed()) + + result, err := cfgctx.GetConfigForData(data, nil) + Expect(err).To(Succeed()) + Expect(config.IsGeneric(result)).To(BeFalse()) + Expect(reflect.TypeOf(result).String()).To(Equal("*config_test.Config")) + }) + + It("it applies to existing context", func() { + RegisterAt(scheme) + + d := newDummy(cfgctx) + + cfg := NewConfig("a", "b") + + err := cfgctx.ApplyConfig(cfg, "test") + + Expect(err).To(Succeed()) + + Expect(d.getApplied()).To(Equal([]*Config{cfg})) + }) + + It("it applies to new context", func() { + RegisterAt(scheme) + + cfg := NewConfig("a", "b") + + err := cfgctx.ApplyConfig(cfg, "test") + Expect(err).To(Succeed()) + + d := newDummy(cfgctx) + Expect(d.applied).To(Equal([]*Config{cfg})) + }) + + It("it applies generic to new context", func() { + cfg := NewConfig("a", "b") + data, err := json.Marshal(cfg) + Expect(err).To(Succeed()) + + gen, err := cfgctx.ApplyData(data, nil, "test") + Expect(err).To(HaveOccurred()) + Expect(errors.IsErrUnknownKind(err, config.KIND_CONFIGTYPE)).To(BeTrue()) + Expect(config.IsGeneric(gen)).To(BeTrue()) + + RegisterAt(scheme) + d := newDummy(cfgctx) + Expect(d.getApplied()).To(Equal([]*Config{cfg})) + }) +}) diff --git a/pkg/contexts/config/cpi/README.md b/api/config/cpi/README.md similarity index 100% rename from pkg/contexts/config/cpi/README.md rename to api/config/cpi/README.md diff --git a/api/config/cpi/config.go b/api/config/cpi/config.go new file mode 100644 index 000000000..332c57d77 --- /dev/null +++ b/api/config/cpi/config.go @@ -0,0 +1,54 @@ +package cpi + +import ( + "strings" + + "ocm.software/ocm/api/config/internal" + "ocm.software/ocm/api/utils/runtime" +) + +type ConfigTypeVersionScheme = runtime.TypeVersionScheme[Config, ConfigType] + +func NewConfigTypeVersionScheme(kind string) ConfigTypeVersionScheme { + return runtime.NewTypeVersionScheme[Config, ConfigType](kind, internal.NewStrictConfigTypeScheme()) +} + +func RegisterConfigType(rtype ConfigType) { + internal.DefaultConfigTypeScheme.Register(rtype) +} + +func RegisterConfigTypeVersions(s ConfigTypeVersionScheme) { + internal.DefaultConfigTypeScheme.AddKnownTypes(s) +} + +//////////////////////////////////////////////////////////////////////////////// + +type configType struct { + runtime.VersionedTypedObjectType[Config] + usage string +} + +func NewConfigType[I Config](name string, usages ...string) ConfigType { + return &configType{ + VersionedTypedObjectType: runtime.NewVersionedTypedObjectType[Config, I](name), + usage: strings.Join(usages, "\n"), + } +} + +func NewConfigTypeyConverter[I Config, V runtime.TypedObject](name string, converter runtime.Converter[I, V], usages ...string) ConfigType { + return &configType{ + VersionedTypedObjectType: runtime.NewVersionedTypedObjectTypeByConverter[Config, I](name, converter), + usage: strings.Join(usages, "\n"), + } +} + +func NewConfigTypeByFormatVersion(name string, fmt runtime.FormatVersion[Config], usages ...string) ConfigType { + return &configType{ + VersionedTypedObjectType: runtime.NewVersionedTypedObjectTypeByFormatVersion[Config](name, fmt), + usage: strings.Join(usages, "\n"), + } +} + +func (t *configType) Usage() string { + return t.usage +} diff --git a/pkg/contexts/config/cpi/content.go b/api/config/cpi/content.go similarity index 93% rename from pkg/contexts/config/cpi/content.go rename to api/config/cpi/content.go index a1e864089..abcc1a78d 100644 --- a/pkg/contexts/config/cpi/content.go +++ b/api/config/cpi/content.go @@ -8,8 +8,8 @@ import ( "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/utils" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type RawData []byte diff --git a/api/config/cpi/interface.go b/api/config/cpi/interface.go new file mode 100644 index 000000000..5785671c2 --- /dev/null +++ b/api/config/cpi/interface.go @@ -0,0 +1,77 @@ +package cpi + +// This is the Context Provider Interface for credential providers + +import ( + "ocm.software/ocm/api/config/internal" + "ocm.software/ocm/api/utils/runtime" +) + +const KIND_CONFIGTYPE = internal.KIND_CONFIGTYPE + +const OCM_CONFIG_TYPE_SUFFIX = internal.OCM_CONFIG_TYPE_SUFFIX + +const CONTEXT_TYPE = internal.CONTEXT_TYPE + +type ( + Context = internal.Context + ContextProvider = internal.ContextProvider + Config = internal.Config + ConfigType = internal.ConfigType + ConfigTypeScheme = internal.ConfigTypeScheme + GenericConfig = internal.GenericConfig + + ConfigSet = internal.ConfigSet + ConfigurationList = internal.ConfigurationList + + ConfigApplier = internal.ConfigApplier + ConfigApplierFunction = internal.ConfigApplierFunction +) + +var DefaultContext = internal.DefaultContext + +func FromProvider(p ContextProvider) Context { + return internal.FromProvider(p) +} + +func NewGenericConfig(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) { + return internal.NewGenericConfig(data, unmarshaler) +} + +func ToGenericConfig(c Config) (*GenericConfig, error) { + return internal.ToGenericConfig(c) +} + +func NewConfigTypeScheme() ConfigTypeScheme { + return internal.NewConfigTypeScheme(nil) +} + +func IsGeneric(cfg Config) bool { + return internal.IsGeneric(cfg) +} + +//////////////////////////////////////////////////////////////////////////////// + +type Updater = internal.Updater + +func NewUpdater(ctx ContextProvider, target interface{}) Updater { + return internal.NewUpdater(ctx, target) +} + +func NewUpdaterForFactory[T any](ctx ContextProvider, f func() T) Updater { + return internal.NewUpdaterForFactory(ctx, f) +} + +//////////////////////////////////////////////////////////////////////////////// + +func ErrNoContext(name string) error { + return internal.ErrNoContext(name) +} + +func IsErrNoContext(err error) bool { + return internal.IsErrNoContext(err) +} + +func IsErrConfigNotApplicable(err error) bool { + return internal.IsErrConfigNotApplicable(err) +} diff --git a/api/config/dummy_test.go b/api/config/dummy_test.go new file mode 100644 index 000000000..0ee190c5f --- /dev/null +++ b/api/config/dummy_test.go @@ -0,0 +1,73 @@ +package config_test + +import ( + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + DummyType = "Dummy" + DummyTypeV1 = DummyType + "/v1" +) + +func RegisterAt(reg cpi.ConfigTypeScheme) { + reg.Register(cpi.NewConfigType[*Config](DummyType)) + reg.Register(cpi.NewConfigType[*Config](DummyTypeV1)) +} + +// Config describes a a dummy config +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + Alice string `json:"alice,omitempty"` + Bob string `json:"bob,omitempty"` +} + +// NewConfig creates a new memory Config +func NewConfig(a, b string) *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(DummyType), + Alice: a, + Bob: b, + } +} + +func (a *Config) GetType() string { + return DummyType +} + +func (a *Config) ApplyTo(ctx config.Context, target interface{}) error { + d, ok := target.(*dummyContext) + if ok { + d.applied = append(d.applied, a) + return nil + } + return cpi.ErrNoContext(DummyType) +} + +//////////////////////////////////////////////////////////////////////////////// + +func newDummy(ctx config.Context) *dummyContext { + d := &dummyContext{ + config: ctx, + } + d.update() + return d +} + +type dummyContext struct { + config config.Context + lastGeneration int64 + applied []*Config +} + +func (d *dummyContext) getApplied() []*Config { + d.update() + return d.applied +} + +func (d *dummyContext) update() error { + gen, err := d.config.ApplyTo(d.lastGeneration, d) + d.lastGeneration = gen + return err +} diff --git a/api/config/extensions/config/context_test.go b/api/config/extensions/config/context_test.go new file mode 100644 index 000000000..daf4e4ea2 --- /dev/null +++ b/api/config/extensions/config/context_test.go @@ -0,0 +1,178 @@ +package config_test + +import ( + "os" + "reflect" + "runtime" + "time" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/general" + "sigs.k8s.io/yaml" + + "ocm.software/ocm/api/config" + local "ocm.software/ocm/api/config/extensions/config" + "ocm.software/ocm/api/datacontext" +) + +func CheckRefs(ctx config.Context, n int) { + runtime.GC() + time.Sleep(time.Second) + Expect(datacontext.GetContextRefCount(ctx)).To(Equal(n)) // all temp refs have been finalized +} + +var _ = Describe("generic config handling", func() { + var scheme config.ConfigTypeScheme + var cfgctx config.Context + + testdataconfig, _ := os.ReadFile("testdata/config.yaml") + testdatajson, _ := yaml.YAMLToJSON(testdataconfig) + + nesteddataconfig, _ := os.ReadFile("testdata/nested.yaml") + + _ = testdatajson + + BeforeEach(func() { + scheme = config.NewConfigTypeScheme() + scheme.AddKnownTypes(config.DefaultContext().ConfigTypes()) + cfgctx = config.WithConfigTypeScheme(scheme).New() + }) + + It("can deserialize config", func() { + result, err := cfgctx.GetConfigForData(testdataconfig, nil) + Expect(err).To(Succeed()) + Expect(config.IsGeneric(result)).To(BeFalse()) + Expect(reflect.TypeOf(result).String()).To(Equal("*config.Config")) + + CheckRefs(cfgctx, 1) + }) + + It("it applies to existing context", func() { + RegisterAt(scheme) + d := newDummy(cfgctx) + + cfg, err := cfgctx.GetConfigForData(testdataconfig, nil) + Expect(err).To(Succeed()) + + err = cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(Succeed()) + gen, cfgs := cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(3))) + Expect(len(cfgs)).To(Equal(3)) + + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("alice", ""), NewConfig("", "bob")})) + + CheckRefs(cfgctx, 1) + }) + + It("it applies nested to existing context", func() { + RegisterAt(scheme) + d := newDummy(cfgctx) + + cfg, err := cfgctx.GetConfigForData(nesteddataconfig, nil) + Expect(err).To(Succeed()) + + err = cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(Succeed()) + gen, cfgs := cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(4))) + Expect(len(cfgs)).To(Equal(4)) + + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("alice", ""), NewConfig("", "bob")})) + + CheckRefs(cfgctx, 1) + }) + + It("it applies unknown type to existing context", func() { + cfg, err := cfgctx.GetConfigForData(testdataconfig, nil) + Expect(err).To(Succeed()) + + err = cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(StringEqualWithContext("testconfig: config apply errors: {config entry 0--testconfig: config type \"Dummy\" is unknown, config entry 1--testconfig: config type \"Dummy\" is unknown}")) + gen, cfgs := cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(3))) + Expect(len(cfgs)).To(Equal(3)) + + RegisterAt(scheme) + d := newDummy(cfgctx) + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("alice", ""), NewConfig("", "bob")})) + + CheckRefs(cfgctx, 1) + }) + + It("it applies composed config to existing context", func() { + RegisterAt(scheme) + d := newDummy(cfgctx) + + cfg := local.New() + + nested := NewConfig("alice", "") + cfg.AddConfig(nested) + nested = NewConfig("", "bob") + cfg.AddConfig(nested) + + err := cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(Succeed()) + gen, cfgs := cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(3))) + Expect(len(cfgs)).To(Equal(3)) + + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("alice", ""), NewConfig("", "bob")})) + + CheckRefs(cfgctx, 1) + }) + + It("it applies composed config set to existing context", func() { + RegisterAt(scheme) + d := newDummy(cfgctx) + + cfg := local.New() + + nested := NewConfig("alice", "") + cfg.AddConfigToSet("test", nested) + nested = NewConfig("", "bob") + cfg.AddConfig(nested) + + err := cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(Succeed()) + gen, cfgs := cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(2))) + Expect(len(cfgs)).To(Equal(2)) + + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("", "bob")})) + + err = cfgctx.ApplyConfigSet("test") + Expect(err).To(Succeed()) + + gen, cfgs = cfgctx.GetConfig(config.AllGenerations, nil) + Expect(gen).To(Equal(int64(3))) + Expect(len(cfgs)).To(Equal(3)) + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("", "bob"), NewConfig("alice", "")})) + + CheckRefs(cfgctx, 1) + }) + + It("it applies compig to storing target", func() { + RegisterAt(scheme) + d := newDummy(cfgctx) + + cfg := NewConfig("alice", "") + + err := cfgctx.ApplyConfig(cfg, "testconfig") + Expect(err).To(Succeed()) + + Expect(d.getApplied()).To(Equal([]*Config{NewConfig("alice", "")})) + + target := dummyTarget{} + MustBeSuccessful(cfgctx.ApplyTo(0, &target)) + Expect(target.used).NotTo(BeNil()) + Expect(target.used.GetId()).To(Equal(cfgctx.GetId())) + + CheckRefs(cfgctx, general.Conditional(datacontext.MULTI_REF, 2, 1)) // config context stored in target with separate ref + target.used.GetId() + }) +}) diff --git a/api/config/extensions/config/dummy_test.go b/api/config/extensions/config/dummy_test.go new file mode 100644 index 000000000..7657404a1 --- /dev/null +++ b/api/config/extensions/config/dummy_test.go @@ -0,0 +1,88 @@ +package config_test + +import ( + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + DummyType = "Dummy" + DummyTypeV1 = DummyType + "/v1" +) + +func RegisterAt(reg cpi.ConfigTypeScheme) { + reg.Register(cpi.NewConfigType[*Config](DummyType)) + reg.Register(cpi.NewConfigType[*Config](DummyTypeV1)) +} + +// Config describes a a dummy config +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + Alice string `json:"alice,omitempty"` + Bob string `json:"bob,omitempty"` +} + +// NewConfig creates a new memory ConfigSpec +func NewConfig(a, b string) *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(DummyType), + Alice: a, + Bob: b, + } +} + +func (a *Config) GetType() string { + return DummyType +} + +func (a *Config) Info() string { + return "dummy config" +} + +func (a *Config) ApplyTo(ctx config.Context, target interface{}) error { + d, ok := target.(*dummyContext) + if ok { + d.applied = append(d.applied, a) + return nil + } + c, ok := target.(*dummyTarget) + if ok { + c.used = ctx + return nil + } + return cpi.ErrNoContext(DummyType) +} + +//////////////////////////////////////////////////////////////////////////////// + +type dummyTarget struct { + used config.Context +} + +//////////////////////////////////////////////////////////////////////////////// + +func newDummy(ctx config.Context) *dummyContext { + d := &dummyContext{ + config: ctx, + } + d.update() + return d +} + +type dummyContext struct { + config config.Context + lastGeneration int64 + applied []*Config +} + +func (d *dummyContext) getApplied() []*Config { + d.update() + return d.applied +} + +func (d *dummyContext) update() error { + gen, err := d.config.ApplyTo(d.lastGeneration, d) + d.lastGeneration = gen + return err +} diff --git a/pkg/contexts/config/config/suite_test.go b/api/config/extensions/config/suite_test.go similarity index 100% rename from pkg/contexts/config/config/suite_test.go rename to api/config/extensions/config/suite_test.go diff --git a/pkg/contexts/config/config/testdata/config.yaml b/api/config/extensions/config/testdata/config.yaml similarity index 100% rename from pkg/contexts/config/config/testdata/config.yaml rename to api/config/extensions/config/testdata/config.yaml diff --git a/pkg/contexts/config/config/testdata/nested.yaml b/api/config/extensions/config/testdata/nested.yaml similarity index 100% rename from pkg/contexts/config/config/testdata/nested.yaml rename to api/config/extensions/config/testdata/nested.yaml diff --git a/api/config/extensions/config/type.go b/api/config/extensions/config/type.go new file mode 100644 index 000000000..dac183377 --- /dev/null +++ b/api/config/extensions/config/type.go @@ -0,0 +1,97 @@ +package config + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ConfigType = "generic" + cpi.OCM_CONFIG_TYPE_SUFFIX + ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" +) + +func init() { + cpi.RegisterConfigType(cpi.NewConfigType[*Config](ConfigType, usage)) + cpi.RegisterConfigType(cpi.NewConfigType[*Config](ConfigTypeV1, usage)) +} + +// Config describes a memory based repository interface. +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + cpi.ConfigurationList `json:",inline"` + Sets map[string]cpi.ConfigSet `json:"sets,omitempty"` +} + +// New creates a new memory ConfigSpec. +func New() *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), + ConfigurationList: cpi.ConfigurationList{[]*cpi.GenericConfig{}}, + Sets: map[string]cpi.ConfigSet{}, + } +} + +func (c *Config) AddSet(name, desc string) { + set := c.Sets[name] + set.Description = desc + c.Sets[name] = set +} + +func (c *Config) AddConfigToSet(name string, cfg cpi.Config) error { + set := c.Sets[name] + err := set.AddConfig(cfg) + if err == nil { + c.Sets[name] = set + } + return err +} + +func (c *Config) GetType() string { + return ConfigType +} + +func (c *Config) ApplyTo(ctx cpi.Context, target interface{}) error { + if cctx, ok := target.(cpi.Context); ok { + for n, s := range c.Sets { + set := s + cctx.AddConfigSet(n, &set) + } + + list := errors.ErrListf("applying generic config list") + for i, cfg := range c.Configurations { + sub := fmt.Sprintf("config entry %d", i) + list.Add(cctx.ApplyConfig(cfg, ctx.WithInfo(sub).Info())) + } + return list.Result() + } + return cpi.ErrNoContext(ConfigType) +} + +const usage = ` +The config type
` + ConfigType + `
can be used to define a list
+of arbitrary configuration specifications and named configuration sets:
+
++ type: ` + ConfigType + ` + configurations: + - type: <any config type> + ... + ... + sets: + standard: + description: my selectable standard config + configurations: + - type: ... + ... + ... ++ +Configurations are directly applied. Configuration sets are +just stored in the configuration context and can be applied +on-demand. On the CLI, this can be done using the main command option +
--config-set <name>
.
+`
diff --git a/api/config/extensions/config/utils.go b/api/config/extensions/config/utils.go
new file mode 100644
index 000000000..fe4a76d7c
--- /dev/null
+++ b/api/config/extensions/config/utils.go
@@ -0,0 +1,60 @@
+package config
+
+import (
+ "ocm.software/ocm/api/config/cpi"
+)
+
+type Aggregator struct {
+ cfg cpi.Config
+ aggr *Config
+ optimized bool
+}
+
+func NewAggregator(optimized bool, cfgs ...cpi.Config) (*Aggregator, error) {
+ a := &Aggregator{optimized: optimized}
+ for _, c := range cfgs {
+ err := a.AddConfig(c)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return a, nil
+}
+
+func (a *Aggregator) Get() cpi.Config {
+ return a.cfg
+}
+
+func (a *Aggregator) AddConfig(cfg cpi.Config) error {
+ if a.cfg == nil {
+ a.cfg = cfg
+ if aggr, ok := cfg.(*Config); ok && a.optimized {
+ a.aggr = aggr
+ }
+ } else {
+ if a.aggr == nil {
+ a.aggr = New()
+ if m, ok := a.cfg.(*Config); ok {
+ // transfer initial config aggregation
+ for _, c := range m.Configurations {
+ err := a.aggr.AddConfig(c)
+ if err != nil {
+ return err
+ }
+ }
+ } else {
+ // add initial config to new aggregation
+ err := a.aggr.AddConfig(a.cfg)
+ if err != nil {
+ return err
+ }
+ }
+ a.cfg = a.aggr
+ }
+ err := a.aggr.AddConfig(cfg)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/api/config/gc_test.go b/api/config/gc_test.go
new file mode 100644
index 000000000..7fe3a74ef
--- /dev/null
+++ b/api/config/gc_test.go
@@ -0,0 +1,34 @@
+package config_test
+
+import (
+ "runtime"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ me "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+var _ = Describe("area test", func() {
+ It("can be garbage collected", func() {
+ ctx := me.New()
+
+ r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx)
+ Expect(r).NotTo(BeNil())
+
+ runtime.GC()
+ time.Sleep(time.Second)
+ ctx.GetType()
+ Expect(r.Get()).To(BeNil())
+
+ ctx = nil
+ for i := 0; i < 100; i++ {
+ runtime.GC()
+ time.Sleep(time.Millisecond)
+ }
+
+ Expect(r.Get()).To(ContainElement(ContainSubstring(me.CONTEXT_TYPE)))
+ })
+})
diff --git a/api/config/init.go b/api/config/init.go
new file mode 100644
index 000000000..ddca39cfe
--- /dev/null
+++ b/api/config/init.go
@@ -0,0 +1,6 @@
+package config
+
+import (
+ _ "ocm.software/ocm/api/config/extensions/config"
+ _ "ocm.software/ocm/api/datacontext/config"
+)
diff --git a/api/config/interface.go b/api/config/interface.go
new file mode 100644
index 000000000..f6f69c406
--- /dev/null
+++ b/api/config/interface.go
@@ -0,0 +1,76 @@
+package config
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/config/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const KIND_CONFIGTYPE = internal.KIND_CONFIGTYPE
+
+const OCM_CONFIG_TYPE_SUFFIX = internal.OCM_CONFIG_TYPE_SUFFIX
+
+const CONTEXT_TYPE = internal.CONTEXT_TYPE
+
+var AllConfigs = internal.AllConfigs
+
+const AllGenerations = internal.AllGenerations
+
+type (
+ Context = internal.Context
+ ContextProvider = internal.ContextProvider
+ Config = internal.Config
+ ConfigType = internal.ConfigType
+ ConfigTypeScheme = internal.ConfigTypeScheme
+ GenericConfig = internal.GenericConfig
+ ConfigSelector = internal.ConfigSelector
+ ConfigSelectorFunction = internal.ConfigSelectorFunction
+ ConfigApplier = internal.ConfigApplier
+ ConfigApplierFunction = internal.ConfigApplierFunction
+)
+
+func DefaultContext() internal.Context {
+ return internal.DefaultContext
+}
+
+func ForContext(ctx context.Context) Context {
+ return internal.FromContext(ctx)
+}
+
+func FromProvider(p ContextProvider) Context {
+ return internal.FromProvider(p)
+}
+
+func DefinedForContext(ctx context.Context) (Context, bool) {
+ return internal.DefinedForContext(ctx)
+}
+
+func NewGenericConfig(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) {
+ return internal.NewGenericConfig(data, unmarshaler)
+}
+
+func ToGenericConfig(c Config) (*GenericConfig, error) {
+ return internal.ToGenericConfig(c)
+}
+
+func NewConfigTypeScheme() ConfigTypeScheme {
+ return internal.NewConfigTypeScheme(nil)
+}
+
+func IsGeneric(cfg Config) bool {
+ return internal.IsGeneric(cfg)
+}
+
+func ErrNoContext(name string) error {
+ return internal.ErrNoContext(name)
+}
+
+func IsErrNoContext(err error) bool {
+ return cpi.IsErrNoContext(err)
+}
+
+func IsErrConfigNotApplicable(err error) bool {
+ return cpi.IsErrConfigNotApplicable(err)
+}
diff --git a/api/config/internal/builder.go b/api/config/internal/builder.go
new file mode 100644
index 000000000..1a0ef061d
--- /dev/null
+++ b/api/config/internal/builder.go
@@ -0,0 +1,69 @@
+package internal
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/datacontext"
+)
+
+type Builder struct {
+ ctx context.Context
+ shared datacontext.AttributesContext
+ reposcheme ConfigTypeScheme
+}
+
+func (b *Builder) getContext() context.Context {
+ if b.ctx == nil {
+ return context.Background()
+ }
+ return b.ctx
+}
+
+func (b Builder) WithContext(ctx context.Context) Builder {
+ b.ctx = ctx
+ return b
+}
+
+func (b Builder) WithSharedAttributes(ctx datacontext.AttributesContext) Builder {
+ b.shared = ctx
+ return b
+}
+
+func (b Builder) WithConfigTypeScheme(scheme ConfigTypeScheme) Builder {
+ b.reposcheme = scheme
+ return b
+}
+
+func (b Builder) Bound() (Context, context.Context) {
+ c := b.New()
+ return c, context.WithValue(b.getContext(), key, c)
+}
+
+func (b Builder) New(m ...datacontext.BuilderMode) Context {
+ mode := datacontext.Mode(m...)
+ ctx := b.getContext()
+
+ if b.shared == nil {
+ if mode == datacontext.MODE_SHARED {
+ b.shared = datacontext.ForContext(ctx)
+ } else {
+ b.shared = datacontext.New(nil)
+ }
+ }
+ if b.reposcheme == nil {
+ switch mode {
+ case datacontext.MODE_INITIAL:
+ b.reposcheme = NewConfigTypeScheme(nil)
+ case datacontext.MODE_CONFIGURED:
+ b.reposcheme = NewConfigTypeScheme(nil)
+ b.reposcheme.AddKnownTypes(DefaultConfigTypeScheme)
+ case datacontext.MODE_EXTENDED:
+ b.reposcheme = NewConfigTypeScheme(nil, DefaultConfigTypeScheme)
+ case datacontext.MODE_DEFAULTED:
+ fallthrough
+ case datacontext.MODE_SHARED:
+ b.reposcheme = DefaultConfigTypeScheme
+ }
+ }
+ return datacontext.SetupContext(mode, newContext(b.shared, b.reposcheme, b.shared))
+}
diff --git a/api/config/internal/builder_test.go b/api/config/internal/builder_test.go
new file mode 100644
index 000000000..6f583c014
--- /dev/null
+++ b/api/config/internal/builder_test.go
@@ -0,0 +1,37 @@
+package internal_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ local "ocm.software/ocm/api/config/internal"
+ "ocm.software/ocm/api/datacontext"
+)
+
+var _ = Describe("builder test", func() {
+ It("creates local", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_SHARED)
+
+ Expect(ctx.AttributesContext()).To(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.ConfigTypes()).To(BeIdenticalTo(local.DefaultConfigTypeScheme))
+ })
+
+ It("creates configured", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_CONFIGURED)
+
+ Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.ConfigTypes()).NotTo(BeIdenticalTo(local.DefaultConfigTypeScheme))
+ Expect(ctx.ConfigTypes().KnownTypeNames()).To(Equal(local.DefaultConfigTypeScheme.KnownTypeNames()))
+ })
+
+ It("creates iniial", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_INITIAL)
+
+ Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.ConfigTypes()).NotTo(BeIdenticalTo(local.DefaultConfigTypeScheme))
+ Expect(len(ctx.ConfigTypes().KnownTypeNames())).To(Equal(0))
+ })
+})
diff --git a/api/config/internal/config.go b/api/config/internal/config.go
new file mode 100644
index 000000000..639e405a8
--- /dev/null
+++ b/api/config/internal/config.go
@@ -0,0 +1,61 @@
+package internal
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const KIND_CONFIGSET = "config set"
+
+type ConfigApplier interface {
+ ApplyConfigTo(Context, cfg, tgt interface{}) error
+}
+
+type Config interface {
+ runtime.VersionedTypedObject
+
+ ApplyTo(Context, interface{}) error
+}
+
+type ConfigApplierFunction func(ctx Context, cfg, tgt interface{}) error
+
+func (f ConfigApplierFunction) ApplyConfigTo(ctx Context, cfg, tgt interface{}) error {
+ return f(ctx, cfg, tgt)
+}
+
+type ConfigSet struct {
+ Description string `json:"description,omitempty"`
+ ConfigurationList `json:",inline"`
+}
+
+type ConfigurationList struct {
+ Configurations []*GenericConfig `json:"configurations,omitempty"`
+}
+
+func (c *ConfigurationList) AddConfig(cfg Config) error {
+ g, err := ToGenericConfig(cfg)
+ if err != nil {
+ return fmt.Errorf("unable to convert config to generic: %w", err)
+ }
+
+ c.Configurations = append(c.Configurations, g)
+
+ return nil
+}
+
+func (c *ConfigurationList) AddConfigData(ctx Context, data []byte) error {
+ cfg, err := ctx.GetConfigForData(data, nil)
+ if err != nil {
+ return errors.Wrapf(err, "invalid config specification")
+ }
+ g, err := ToGenericConfig(cfg)
+ if err != nil {
+ return fmt.Errorf("unable to convert config to generic: %w", err)
+ }
+
+ c.Configurations = append(c.Configurations, g)
+ return nil
+}
diff --git a/pkg/contexts/config/internal/configtypes.go b/api/config/internal/configtypes.go
similarity index 97%
rename from pkg/contexts/config/internal/configtypes.go
rename to api/config/internal/configtypes.go
index ac0381952..a4a2cd55d 100644
--- a/pkg/contexts/config/internal/configtypes.go
+++ b/api/config/internal/configtypes.go
@@ -7,8 +7,8 @@ import (
"github.com/mandelsoft/goutils/errors"
"github.com/modern-go/reflect2"
- "github.com/open-component-model/ocm/pkg/runtime"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
)
type ConfigType interface {
diff --git a/api/config/internal/context.go b/api/config/internal/context.go
new file mode 100644
index 000000000..de8c12283
--- /dev/null
+++ b/api/config/internal/context.go
@@ -0,0 +1,356 @@
+package internal
+
+import (
+ "context"
+ "reflect"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// OCM_CONFIG_TYPE_SUFFIX is the standard suffix used for configuration
+// types provided by this library.
+const OCM_CONFIG_TYPE_SUFFIX = ".config" + common.OCM_TYPE_GROUP_SUFFIX
+
+type ConfigSelector interface {
+ Select(Config) bool
+}
+type ConfigSelectorFunction func(Config) bool
+
+func (f ConfigSelectorFunction) Select(cfg Config) bool { return f(cfg) }
+
+var AllConfigs = AppliedConfigSelectorFunction(func(*AppliedConfig) bool { return true })
+
+const AllGenerations int64 = 0
+
+const CONTEXT_TYPE = "config" + datacontext.OCM_CONTEXT_SUFFIX
+
+type ContextProvider interface {
+ ConfigContext() Context
+}
+
+type Context interface {
+ datacontext.Context
+ ContextProvider
+
+ AttributesContext() datacontext.AttributesContext
+
+ // Info provides the context for nested configuration evaluation
+ Info() string
+ // WithInfo provides the same context with additional nesting info
+ WithInfo(desc string) Context
+
+ ConfigTypes() ConfigTypeScheme
+
+ // SkipUnknownConfig can be used to control the behaviour
+ // for processing unknown configuration object types.
+ // It returns the previous mode valid before setting the
+ // new one.
+ SkipUnknownConfig(bool) bool
+
+ // Validate validates the applied configuration for not using
+ // unknown configuration types, anymore. This can be used after setting
+ // SkipUnknownConfig, to check whether there are still unknown types
+ // which will be skipped. It does not provide information, whether
+ // config objects were skipped for previous object configuration
+ // requests.
+ Validate() error
+
+ // GetConfigForData deserialize configuration objects for known
+ // configuration types.
+ GetConfigForData(data []byte, unmarshaler runtime.Unmarshaler) (Config, error)
+
+ // ApplyData applies the config given by a byte stream to the config store
+ // If the config type is not known, a generic config is stored and returned.
+ // In this case an unknown error for kind KIND_CONFIGTYPE is returned.
+ ApplyData(data []byte, unmarshaler runtime.Unmarshaler, desc string) (Config, error)
+ // ApplyConfig applies the config to the config store
+ ApplyConfig(spec Config, desc string) error
+
+ GetConfigForType(generation int64, typ string) (int64, []Config)
+ GetConfigForName(generation int64, name string) (int64, []Config)
+ GetConfig(generation int64, selector ConfigSelector) (int64, []Config)
+
+ AddConfigSet(name string, set *ConfigSet)
+ ApplyConfigSet(name string) error
+
+ // Reset all configs applied so far, subsequent calls to ApplyTo will
+ // ony see configs allpied after the last reset.
+ Reset() int64
+ // Generation return the actual config generation.
+ // this is a strictly increasing number, regardless of the number
+ // of Reset calls.
+ Generation() int64
+ // ApplyTo applies all configurations applied after the last reset with
+ // a generation larger than the given watermark to the specified target.
+ // A target may be any object. The applied configuration objects decide
+ // on their own whether they are applicable for the given target.
+ // The generation of the last applied object is returned to be used as
+ // new watermark.
+ ApplyTo(gen int64, target interface{}) (int64, error)
+}
+
+var key = reflect.TypeOf(_context{})
+
+// DefaultContext is the default context initialized by init functions.
+var DefaultContext = Builder{}.New(datacontext.MODE_SHARED)
+
+// FromContext returns the Context to use for context.Context.
+// This is either an explicit context or the default context.
+// The returned context incorporates the given context.
+func FromContext(ctx context.Context) Context {
+ c, _ := datacontext.ForContextByKey(ctx, key, DefaultContext)
+ return c.(Context)
+}
+
+func FromProvider(p ContextProvider) Context {
+ if p == nil {
+ return nil
+ }
+ return p.ConfigContext()
+}
+
+func DefinedForContext(ctx context.Context) (Context, bool) {
+ c, ok := datacontext.ForContextByKey(ctx, key, DefaultContext)
+ if c != nil {
+ return c.(Context), ok
+ }
+ return nil, ok
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type _InternalContext = datacontext.InternalContext
+
+type coreContext struct {
+ _InternalContext
+ updater Updater
+
+ sharedAttributes datacontext.AttributesContext
+
+ knownConfigTypes ConfigTypeScheme
+
+ configs *ConfigStore
+ skipUnknownConfig bool
+}
+
+type _context struct {
+ *coreContext
+ description string
+}
+
+var (
+ _ Context = (*_context)(nil)
+ _ datacontext.ViewCreator[Context] = (*_context)(nil)
+)
+
+// gcWrapper is used as garbage collectable
+// wrapper for a context implementation
+// to establish a runtime finalizer.
+type gcWrapper struct {
+ datacontext.GCWrapper
+ *_context
+}
+
+func newView(c *_context, ref ...bool) Context {
+ if utils.Optional(ref...) {
+ return datacontext.FinalizedContext[gcWrapper](c)
+ }
+ return c
+}
+
+func (w *gcWrapper) SetContext(c *_context) {
+ w._context = c
+}
+
+func newContext(shared datacontext.AttributesContext, reposcheme ConfigTypeScheme, delegates datacontext.Delegates) Context {
+ c := &_context{
+ coreContext: &coreContext{
+ sharedAttributes: shared,
+ knownConfigTypes: reposcheme,
+ configs: NewConfigStore(),
+ },
+ }
+ c._InternalContext = datacontext.NewContextBase(c, CONTEXT_TYPE, key, shared.GetAttributes(), delegates)
+ c.updater = NewUpdaterForFactory(c, c.ConfigContext) // provide target as new view to internal context
+ datacontext.AssureUpdater(shared, NewUpdater(c, datacontext.PersistentContextRef(shared)))
+
+ return newView(c, true)
+}
+
+func (c *_context) CreateView() Context {
+ return newView(c, true)
+}
+
+func (c *_context) ConfigContext() Context {
+ return newView(c)
+}
+
+func (c *_context) Update() error {
+ return c.updater.Update()
+}
+
+var _ datacontext.Updater = (*_context)(nil)
+
+func (c *_context) Info() string {
+ return c.description
+}
+
+func (c *_context) WithInfo(desc string) Context {
+ if c.description != "" {
+ desc = desc + "--" + c.description
+ }
+ return newView(&_context{c.coreContext, desc})
+}
+
+func (c *_context) AttributesContext() datacontext.AttributesContext {
+ c.updater.Update()
+ return c.sharedAttributes
+}
+
+func (c *_context) ConfigTypes() ConfigTypeScheme {
+ return c.knownConfigTypes
+}
+
+func (c *_context) SkipUnknownConfig(b bool) bool {
+ old := c.skipUnknownConfig
+ c.skipUnknownConfig = b
+ return old
+}
+
+func (c *_context) ConfigForData(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) {
+ return c.knownConfigTypes.Decode(data, unmarshaler)
+}
+
+func (c *_context) GetConfigForData(data []byte, unmarshaler runtime.Unmarshaler) (Config, error) {
+ spec, err := c.knownConfigTypes.Decode(data, unmarshaler)
+ if err != nil {
+ return nil, err
+ }
+ return spec, nil
+}
+
+func (c *_context) ApplyConfig(spec Config, desc string) error {
+ var unknown error
+
+ // use temporary view for outbound calls
+ spec, err := (&AppliedConfig{config: spec}).eval(newView(c))
+ if err != nil {
+ if !errors.IsErrUnknownKind(err, KIND_CONFIGTYPE) {
+ return errors.Wrapf(err, "%s", desc)
+ }
+ if !c.skipUnknownConfig {
+ unknown = err
+ }
+ err = nil
+ }
+
+ c.configs.Apply(spec, desc)
+
+ for {
+ // apply directly and also indirectly described configurations
+ if gen, in := c.updater.State(); err != nil || in || gen >= c.configs.Generation() {
+ break
+ }
+ err = c.Update()
+ if IsErrNoContext(err) {
+ err = unknown
+ }
+ }
+
+ return errors.Wrapf(err, "%s", desc)
+}
+
+func (c *_context) ApplyData(data []byte, unmarshaler runtime.Unmarshaler, desc string) (Config, error) {
+ spec, err := c.knownConfigTypes.Decode(data, unmarshaler)
+ if err != nil {
+ return nil, err
+ }
+ return spec, c.ApplyConfig(spec, desc)
+}
+
+func (c *_context) selector(gen int64, selector ConfigSelector) AppliedConfigSelector {
+ if gen <= 0 {
+ return AppliedConfigSelectorFor(selector)
+ }
+ if selector == nil {
+ return AppliedGenerationSelector(gen)
+ }
+ return AppliedAndSelector(AppliedGenerationSelector(gen), AppliedConfigSelectorFor(selector))
+}
+
+func (c *_context) Generation() int64 {
+ return c.configs.Generation()
+}
+
+func (c *_context) Reset() int64 {
+ return c.configs.Reset()
+}
+
+func (c *_context) ApplyTo(gen int64, target interface{}) (int64, error) {
+ cur := c.configs.Generation()
+ if cur <= gen {
+ return gen, nil
+ }
+ cur, cfgs := c.configs.GetConfigForSelector(c, AppliedGenerationSelector(gen))
+
+ list := errors.ErrListf("config apply errors")
+ for _, cfg := range cfgs {
+ err := cfg.config.ApplyTo(c.WithInfo(cfg.description), target)
+ if c.skipUnknownConfig && errors.IsErrUnknownKind(err, KIND_CONFIGTYPE) {
+ err = nil
+ }
+ err = errors.Wrapf(err, "%s", cfg.description)
+ if !IsErrNoContext(err) {
+ list.Add(err)
+ }
+ }
+ return cur, list.Result()
+}
+
+func (c *_context) Validate() error {
+ list := errors.ErrList()
+
+ _, cfgs := c.configs.GetConfigForSelector(c, AllAppliedConfigs)
+ for _, cfg := range cfgs {
+ _, err := cfg.eval(newView(c))
+ list.Add(err)
+ }
+ return list.Result()
+}
+
+func (c *_context) AddConfigSet(name string, set *ConfigSet) {
+ c.configs.AddSet(name, set)
+}
+
+func (c *_context) ApplyConfigSet(name string) error {
+ set := c.configs.GetSet(name)
+ if set == nil {
+ return errors.ErrUnknown(KIND_CONFIGSET, name)
+ }
+ desc := "config set " + name
+ list := errors.ErrListf("applying %s", desc)
+ for _, cfg := range set.Configurations {
+ list.Add(c.ApplyConfig(cfg, desc))
+ }
+ return list.Result()
+}
+
+func (c *_context) GetConfig(gen int64, selector ConfigSelector) (int64, []Config) {
+ gen, cfgs := c.configs.GetConfigForSelector(c, c.selector(gen, selector))
+ return gen, cfgs.Configs()
+}
+
+func (c *_context) GetConfigForName(gen int64, name string) (int64, []Config) {
+ gen, cfgs := c.configs.GetConfigForName(c, name, c.selector(gen, nil))
+ return gen, cfgs.Configs()
+}
+
+func (c *_context) GetConfigForType(gen int64, typ string) (int64, []Config) {
+ gen, cfgs := c.configs.GetConfigForType(c, typ, c.selector(gen, nil))
+ return gen, cfgs.Configs()
+}
diff --git a/pkg/contexts/config/internal/errors.go b/api/config/internal/errors.go
similarity index 100%
rename from pkg/contexts/config/internal/errors.go
rename to api/config/internal/errors.go
diff --git a/api/config/internal/logging.go b/api/config/internal/logging.go
new file mode 100644
index 000000000..ca8676f5c
--- /dev/null
+++ b/api/config/internal/logging.go
@@ -0,0 +1,9 @@
+package internal
+
+import (
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var Realm = ocmlog.DefineSubRealm("configuration management", "config")
+
+var Logger = ocmlog.DynamicLogger(Realm)
diff --git a/api/config/internal/setup_test.go b/api/config/internal/setup_test.go
new file mode 100644
index 000000000..731f230cb
--- /dev/null
+++ b/api/config/internal/setup_test.go
@@ -0,0 +1,16 @@
+package internal_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/config/internal"
+)
+
+var _ = Describe("setup", func() {
+ It("creates initial", func() {
+ Expect(len(config.DefaultContext().ConfigTypes().KnownTypeNames())).To(Equal(6))
+ Expect(len(internal.DefaultConfigTypeScheme.KnownTypeNames())).To(Equal(6))
+ })
+})
diff --git a/pkg/contexts/config/internal/store.go b/api/config/internal/store.go
similarity index 100%
rename from pkg/contexts/config/internal/store.go
rename to api/config/internal/store.go
diff --git a/pkg/contexts/config/internal/suite_test.go b/api/config/internal/suite_test.go
similarity index 100%
rename from pkg/contexts/config/internal/suite_test.go
rename to api/config/internal/suite_test.go
diff --git a/pkg/contexts/config/internal/updater.go b/api/config/internal/updater.go
similarity index 100%
rename from pkg/contexts/config/internal/updater.go
rename to api/config/internal/updater.go
diff --git a/api/config/logging.go b/api/config/logging.go
new file mode 100644
index 000000000..ad7501506
--- /dev/null
+++ b/api/config/logging.go
@@ -0,0 +1,17 @@
+package config
+
+import (
+ "ocm.software/ocm/api/config/internal"
+)
+
+var Realm = internal.Realm
+
+var Logger = internal.Logger
+
+func Debug(c Context, msg string, keypairs ...interface{}) {
+ c.LoggingContext().Logger(Realm).Debug(msg, append(keypairs, "id", c.GetId())...)
+}
+
+func Info(c Context, msg string, keypairs ...interface{}) {
+ c.LoggingContext().Logger(Realm).Info(msg, append(keypairs, "id", c.GetId())...)
+}
diff --git a/api/config/plugin/type.go b/api/config/plugin/type.go
new file mode 100644
index 000000000..1030f2747
--- /dev/null
+++ b/api/config/plugin/type.go
@@ -0,0 +1,21 @@
+package plugin
+
+import (
+ "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/config/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+var _ cpi.Config = (*Config)(nil)
+
+type Config struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+}
+
+func (c *Config) ApplyTo(context internal.Context, i interface{}) error {
+ return nil
+}
+
+func New(name string, desc string) cpi.ConfigType {
+ return cpi.NewConfigType[*Config](name, desc)
+}
diff --git a/pkg/contexts/config/suite_test.go b/api/config/suite_test.go
similarity index 100%
rename from pkg/contexts/config/suite_test.go
rename to api/config/suite_test.go
diff --git a/pkg/contexts/credentials/area_test.go b/api/credentials/area_test.go
similarity index 91%
rename from pkg/contexts/credentials/area_test.go
rename to api/credentials/area_test.go
index 7bc91a31f..ed104f822 100644
--- a/pkg/contexts/credentials/area_test.go
+++ b/api/credentials/area_test.go
@@ -7,7 +7,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- "github.com/open-component-model/ocm/pkg/contexts/credentials"
+ "ocm.software/ocm/api/credentials"
)
var DefaultContext = credentials.New()
diff --git a/api/credentials/builder.go b/api/credentials/builder.go
new file mode 100644
index 000000000..a9df8bd1e
--- /dev/null
+++ b/api/credentials/builder.go
@@ -0,0 +1,29 @@
+package credentials
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/datacontext"
+)
+
+func WithContext(ctx context.Context) internal.Builder {
+ return internal.Builder{}.WithContext(ctx)
+}
+
+func WithConfigs(ctx config.Context) internal.Builder {
+ return internal.Builder{}.WithConfig(ctx)
+}
+
+func WithRepositoyTypeScheme(scheme RepositoryTypeScheme) internal.Builder {
+ return internal.Builder{}.WithRepositoyTypeScheme(scheme)
+}
+
+func WithStandardConumerMatchers(matchers internal.IdentityMatcherRegistry) internal.Builder {
+ return internal.Builder{}.WithStandardConumerMatchers(matchers)
+}
+
+func New(mode ...datacontext.BuilderMode) Context {
+ return internal.Builder{}.New(mode...)
+}
diff --git a/pkg/contexts/credentials/builtin/github/ghcr.go b/api/credentials/builtin/github/ghcr.go
similarity index 75%
rename from pkg/contexts/credentials/builtin/github/ghcr.go
rename to api/credentials/builtin/github/ghcr.go
index 9c664afc7..e82fcd909 100644
--- a/pkg/contexts/credentials/builtin/github/ghcr.go
+++ b/api/credentials/builtin/github/ghcr.go
@@ -3,9 +3,9 @@ package github
import (
"os"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/oci/identity"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
+ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ common "ocm.software/ocm/api/utils/misc"
)
const HOST = "ghcr.io"
diff --git a/api/credentials/builtin/github/github.go b/api/credentials/builtin/github/github.go
new file mode 100644
index 000000000..b46f2225f
--- /dev/null
+++ b/api/credentials/builtin/github/github.go
@@ -0,0 +1,22 @@
+package github
+
+import (
+ "os"
+
+ "ocm.software/ocm/api/credentials/builtin/github/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+func init() {
+ t := os.Getenv("GITHUB_TOKEN")
+ if t != "" {
+ us := os.Getenv("GITHUB_SERVER_URL")
+ id := identity.GetConsumerId(us)
+
+ if src, err := cpi.DefaultContext.GetCredentialsForConsumer(id); err != nil || src == nil {
+ creds := cpi.NewCredentials(common.Properties{cpi.ATTR_TOKEN: t})
+ cpi.DefaultContext.SetCredentialsForConsumer(id, creds)
+ }
+ }
+}
diff --git a/api/credentials/builtin/github/identity/identity.go b/api/credentials/builtin/github/identity/identity.go
new file mode 100644
index 000000000..facaf68c5
--- /dev/null
+++ b/api/credentials/builtin/github/identity/identity.go
@@ -0,0 +1,83 @@
+package identity
+
+import (
+ "net/url"
+ "path"
+ "strings"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+const CONSUMER_TYPE = "Github"
+
+// identity properties
+const (
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+)
+
+// credential properties
+const (
+ ATTR_TOKEN = cpi.ATTR_TOKEN
+)
+
+const GITHUB = "github.com"
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_TOKEN, "GitHub personal access token",
+ })
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, identityMatcher,
+ `GitHub credential matcher
+
+This matcher is a hostpath matcher.`,
+ attrs)
+}
+
+func PATCredentials(pat string) cpi.Credentials {
+ return cpi.DirectCredentials{
+ ATTR_TOKEN: pat,
+ }
+}
+
+func GetConsumerId(serverurl string, repo ...string) cpi.ConsumerIdentity {
+ host := GITHUB
+ port := ""
+ if serverurl != "" {
+ u, err := url.Parse(serverurl)
+ if err == nil {
+ host = u.Host
+ }
+ }
+ if idx := strings.Index(host, ":"); idx > 0 {
+ port = host[idx+1:]
+ host = host[:idx]
+ }
+
+ id := cpi.ConsumerIdentity{
+ cpi.ID_TYPE: CONSUMER_TYPE,
+ ID_HOSTNAME: host,
+ }
+ if port != "" {
+ id[ID_PORT] = port
+ }
+ p := path.Join(repo...)
+ if p != "" {
+ id[ID_PATHPREFIX] = p
+ }
+ return id
+}
+
+func GetCredentials(ctx cpi.ContextProvider, serverurl string, repo ...string) (cpi.Credentials, error) {
+ id := GetConsumerId(serverurl, repo...)
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id, identityMatcher)
+}
diff --git a/api/credentials/builtin/helm/identity/identity.go b/api/credentials/builtin/helm/identity/identity.go
new file mode 100644
index 000000000..b4397de1e
--- /dev/null
+++ b/api/credentials/builtin/helm/identity/identity.go
@@ -0,0 +1,103 @@
+package identity
+
+import (
+ "strings"
+
+ "helm.sh/helm/v3/pkg/registry"
+
+ ociidentity "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+// CONSUMER_TYPE is the Helm chart repository type.
+const CONSUMER_TYPE = "HelmChartRepository"
+
+// ID_TYPE is the type field of a consumer identity.
+const ID_TYPE = cpi.ID_TYPE
+
+// ID_SCHEME is the scheme of the repository.
+const ID_SCHEME = hostpath.ID_SCHEME
+
+// ID_HOSTNAME is the hostname of a repository.
+const ID_HOSTNAME = hostpath.ID_HOSTNAME
+
+// ID_PORT is the port number of a repository.
+const ID_PORT = hostpath.ID_PORT
+
+// ID_PATHPREFIX is the path of a repository.
+const ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "the basic auth user name",
+ ATTR_PASSWORD, "the basic auth password",
+ ATTR_CERTIFICATE, "TLS client certificate",
+ ATTR_PRIVATE_KEY, "TLS private key",
+ ATTR_CERTIFICATE_AUTHORITY, "TLS certificate authority",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher, `Helm chart repository
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+var identityMatcher = hostpath.IdentityMatcher("")
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+// used credential attributes
+
+const (
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+ ATTR_CERTIFICATE_AUTHORITY = cpi.ATTR_CERTIFICATE_AUTHORITY
+ ATTR_CERTIFICATE = cpi.ATTR_CERTIFICATE
+ ATTR_PRIVATE_KEY = cpi.ATTR_PRIVATE_KEY
+)
+
+func OCIRepoURL(repourl string, chartname string) string {
+ repourl = strings.TrimSuffix(repourl, "/")[3+len(registry.OCIScheme):]
+ if chartname != "" {
+ repourl += "/" + chartname
+ }
+ return repourl
+}
+
+func SimpleCredentials(user, passwd string) cpi.Credentials {
+ return cpi.DirectCredentials{
+ ATTR_USERNAME: user,
+ ATTR_PASSWORD: passwd,
+ }
+}
+
+func GetConsumerId(repourl string, chartname string) cpi.ConsumerIdentity {
+ i := strings.LastIndex(chartname, ":")
+ if i >= 0 {
+ chartname = chartname[:i]
+ }
+ if registry.IsOCI(repourl) {
+ repourl = strings.TrimSuffix(repourl, "/")
+ return ociidentity.GetConsumerId(OCIRepoURL(repourl, ""), chartname)
+ } else {
+ return hostpath.GetConsumerIdentity(CONSUMER_TYPE, repourl)
+ }
+}
+
+func GetCredentials(ctx cpi.ContextProvider, repourl string, chartname string) common.Properties {
+ id := GetConsumerId(repourl, chartname)
+ if id == nil {
+ return nil
+ }
+ creds, err := cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
+ if creds == nil || err != nil {
+ return nil
+ }
+ return creds.Properties()
+}
diff --git a/api/credentials/builtin/helm/identity/identity_test.go b/api/credentials/builtin/helm/identity/identity_test.go
new file mode 100644
index 000000000..ddefea679
--- /dev/null
+++ b/api/credentials/builtin/helm/identity/identity_test.go
@@ -0,0 +1,77 @@
+package identity_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/credentials/builtin/helm/identity"
+
+ "ocm.software/ocm/api/credentials"
+ ociidentity "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/oci"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+var _ = Describe("consumer id handling", func() {
+ Context("id deternation", func() {
+ It("handles helm repos", func() {
+ id := GetConsumerId("https://acme.org/charts", "demo:v1")
+ Expect(id).To(Equal(credentials.NewConsumerIdentity(CONSUMER_TYPE,
+ "pathprefix", "charts",
+ "port", "443",
+ "hostname", "acme.org",
+ "scheme", "https",
+ )))
+ })
+
+ It("handles oci repos", func() {
+ id := GetConsumerId("oci://acme.org/charts", "demo:v1")
+ Expect(id).To(Equal(credentials.NewConsumerIdentity(ociidentity.CONSUMER_TYPE,
+ "pathprefix", "charts/demo",
+ "hostname", "acme.org",
+ )))
+ })
+ })
+
+ Context("query credentials", func() {
+ var ctx oci.Context
+ var credctx credentials.Context
+
+ BeforeEach(func() {
+ ctx = oci.New(datacontext.MODE_EXTENDED)
+ credctx = ctx.CredentialsContext()
+ })
+
+ It("queries helm credentials", func() {
+ id := GetConsumerId("https://acme.org/charts", "demo:v1")
+ credctx.SetCredentialsForConsumer(id,
+ credentials.CredentialsFromList(
+ ATTR_USERNAME, "helm",
+ ATTR_PASSWORD, "helmpass",
+ ),
+ )
+
+ creds := GetCredentials(ctx, "https://acme.org/charts", "demo:v1")
+ Expect(creds).To(Equal(common.Properties{
+ ATTR_USERNAME: "helm",
+ ATTR_PASSWORD: "helmpass",
+ }))
+ })
+
+ It("queries oci credentials", func() {
+ id := GetConsumerId("oci://acme.org/charts", "demo:v1")
+ credctx.SetCredentialsForConsumer(id,
+ credentials.CredentialsFromList(
+ ATTR_USERNAME, "oci",
+ ATTR_PASSWORD, "ocipass",
+ ),
+ )
+
+ creds := GetCredentials(ctx, "oci://acme.org/charts", "demo:v1")
+ Expect(creds).To(Equal(common.Properties{
+ ATTR_USERNAME: "oci",
+ ATTR_PASSWORD: "ocipass",
+ }))
+ })
+ })
+})
diff --git a/pkg/contexts/credentials/builtin/helm/identity/suite_test.go b/api/credentials/builtin/helm/identity/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/builtin/helm/identity/suite_test.go
rename to api/credentials/builtin/helm/identity/suite_test.go
diff --git a/api/credentials/builtin/init.go b/api/credentials/builtin/init.go
new file mode 100644
index 000000000..210a22450
--- /dev/null
+++ b/api/credentials/builtin/init.go
@@ -0,0 +1,8 @@
+package builtin
+
+import (
+ _ "ocm.software/ocm/api/credentials/builtin/github"
+ _ "ocm.software/ocm/api/credentials/builtin/helm/identity"
+ _ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ _ "ocm.software/ocm/api/credentials/builtin/wget/identity"
+)
diff --git a/api/credentials/builtin/maven/identity/identity.go b/api/credentials/builtin/maven/identity/identity.go
new file mode 100644
index 000000000..14b89c53b
--- /dev/null
+++ b/api/credentials/builtin/maven/identity/identity.go
@@ -0,0 +1,62 @@
+package identity
+
+import (
+ . "net/url"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+ "ocm.software/ocm/api/utils/logging"
+)
+
+const (
+ // CONSUMER_TYPE is the maven repository type.
+ CONSUMER_TYPE = "MavenRepository"
+
+ // ATTR_USERNAME is the username attribute. Required for login at any maven registry.
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ // ATTR_PASSWORD is the password attribute. Required for login at any maven registry.
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+)
+
+// REALM the logging realm / prefix.
+var REALM = logging.DefineSubRealm("Maven repository", "maven")
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "the basic auth user name",
+ ATTR_PASSWORD, "the basic auth password",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `MVN repository
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func GetConsumerId(rawURL, groupId string) (cpi.ConsumerIdentity, error) {
+ url, err := JoinPath(rawURL, groupId)
+ if err != nil {
+ return nil, err
+ }
+ return hostpath.GetConsumerIdentity(CONSUMER_TYPE, url), nil
+}
+
+func GetCredentials(ctx cpi.ContextProvider, repoUrl, groupId string) (cpi.Credentials, error) {
+ id, err := GetConsumerId(repoUrl, groupId)
+ if err != nil {
+ return nil, err
+ }
+ if id == nil {
+ logging.DynamicLogger(REALM).Debug("No consumer identity found.", "url", repoUrl, "groupId", groupId)
+ return nil, nil
+ }
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
+}
diff --git a/api/credentials/builtin/npm/identity/identity.go b/api/credentials/builtin/npm/identity/identity.go
new file mode 100644
index 000000000..d65390686
--- /dev/null
+++ b/api/credentials/builtin/npm/identity/identity.go
@@ -0,0 +1,68 @@
+package identity
+
+import (
+ "net/url"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+ "ocm.software/ocm/api/utils/logging"
+)
+
+const (
+ // CONSUMER_TYPE is the npm repository type.
+ CONSUMER_TYPE = "NpmRegistry"
+
+ // ATTR_USERNAME is the username attribute. Required for login at any npm registry.
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ // ATTR_PASSWORD is the password attribute. Required for login at any npm registry.
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+ // ATTR_EMAIL is the email attribute. Required for login at any npm registry.
+ ATTR_EMAIL = cpi.ATTR_EMAIL
+ // ATTR_TOKEN is the token attribute. May exist after login at any npm registry.
+ ATTR_TOKEN = cpi.ATTR_TOKEN
+)
+
+// Logging Realm.
+var REALM = logging.DefineSubRealm("NPM registry", "npm")
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "the basic auth user name",
+ ATTR_PASSWORD, "the basic auth password",
+ ATTR_EMAIL, "NPM registry, require an email address",
+ ATTR_TOKEN, "the token attribute. May exist after login at any npm registry. Check your .npmrc file!",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `NPM registry
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func GetConsumerId(rawURL, groupId string) (cpi.ConsumerIdentity, error) {
+ _url, err := url.JoinPath(rawURL, groupId)
+ if err != nil {
+ return nil, err
+ }
+ return hostpath.GetConsumerIdentity(CONSUMER_TYPE, _url), nil
+}
+
+func GetCredentials(ctx cpi.ContextProvider, repoUrl string, pkgName string) (cpi.Credentials, error) {
+ id, err := GetConsumerId(repoUrl, pkgName)
+ if err != nil {
+ return nil, err
+ }
+ if id == nil {
+ logging.DynamicLogger(REALM).Debug("No consumer identity found.", "url", repoUrl, "groupId", pkgName)
+ return nil, nil
+ }
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
+}
diff --git a/pkg/contexts/credentials/builtin/oci/identity/creds.go b/api/credentials/builtin/oci/identity/creds.go
similarity index 86%
rename from pkg/contexts/credentials/builtin/oci/identity/creds.go
rename to api/credentials/builtin/oci/identity/creds.go
index 972f8cf9c..d2988ac20 100644
--- a/pkg/contexts/credentials/builtin/oci/identity/creds.go
+++ b/api/credentials/builtin/oci/identity/creds.go
@@ -3,8 +3,8 @@ package identity
import (
"path"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils"
)
func SimpleCredentials(user, passwd string) cpi.Credentials {
diff --git a/api/credentials/builtin/oci/identity/id_test.go b/api/credentials/builtin/oci/identity/id_test.go
new file mode 100644
index 000000000..82fbb6239
--- /dev/null
+++ b/api/credentials/builtin/oci/identity/id_test.go
@@ -0,0 +1,145 @@
+package identity_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+)
+
+var _ = Describe("ctf management", func() {
+ Context("with path", func() {
+ pat := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a/b",
+ identity.ID_PORT: "4711",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a/b",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("path prefix", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "b",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("longer prefix", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a/b/c",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing path", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a/b",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+
+ Expect(identity.IdentityMatcher(id, nil, pat)).To(BeTrue()) // accept additional port as fallback
+ Expect(identity.IdentityMatcher(id, id, pat)).To(BeFalse()) // but not to replace more general match
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PATHPREFIX: "a/b",
+ identity.ID_PORT: "0815",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+
+ It("different host", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "other",
+ identity.ID_PATHPREFIX: "a/b",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("no host", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_PATHPREFIX: "a/b",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(id, nil, pat)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, id, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, id, pat)).To(BeTrue())
+ })
+ })
+
+ Context("without path", func() {
+ pat := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PORT: "4711",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PORT: "4711",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PORT: "4711",
+ identity.ID_PATHPREFIX: "b",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ identity.ID_HOSTNAME: "host",
+ identity.ID_PORT: "0815",
+ }
+ Expect(identity.IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(identity.IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ })
+})
diff --git a/api/credentials/builtin/oci/identity/identity.go b/api/credentials/builtin/oci/identity/identity.go
new file mode 100644
index 000000000..004d477a9
--- /dev/null
+++ b/api/credentials/builtin/oci/identity/identity.go
@@ -0,0 +1,48 @@
+package identity
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+// CONSUMER_TYPE is the OCT registry type.
+const CONSUMER_TYPE = "OCIRegistry"
+
+// used identity properties.
+const (
+ ID_TYPE = hostpath.ID_TYPE
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+ ID_SCHEME = hostpath.ID_SCHEME
+)
+
+// used credential properties.
+const (
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+ ATTR_IDENTITY_TOKEN = cpi.ATTR_IDENTITY_TOKEN
+ ATTR_CERTIFICATE_AUTHORITY = cpi.ATTR_CERTIFICATE_AUTHORITY
+)
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "the basic auth user name",
+ ATTR_PASSWORD, "the basic auth password",
+ ATTR_IDENTITY_TOKEN, "the bearer token used for non-basic auth authorization",
+ ATTR_CERTIFICATE_AUTHORITY, "the certificate authority certificate used to verify certificates",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher, `OCI registry credential matcher
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
diff --git a/pkg/contexts/credentials/builtin/oci/identity/suite_test.go b/api/credentials/builtin/oci/identity/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/builtin/oci/identity/suite_test.go
rename to api/credentials/builtin/oci/identity/suite_test.go
diff --git a/api/credentials/builtin/wget/identity/identity.go b/api/credentials/builtin/wget/identity/identity.go
new file mode 100644
index 000000000..508c13e0c
--- /dev/null
+++ b/api/credentials/builtin/wget/identity/identity.go
@@ -0,0 +1,56 @@
+package identity
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+// CONSUMER_TYPE is the wget access method type.
+const CONSUMER_TYPE = "wget"
+
+// used identity properties.
+const (
+ ID_TYPE = hostpath.ID_TYPE
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+ ID_SCHEME = hostpath.ID_SCHEME
+)
+
+// used credential properties.
+const (
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+ ATTR_IDENTITY_TOKEN = cpi.ATTR_IDENTITY_TOKEN
+ ATTR_CERTIFICATE_AUTHORITY = cpi.ATTR_CERTIFICATE_AUTHORITY
+ ATTR_CERTIFICATE = cpi.ATTR_CERTIFICATE
+ ATTR_PRIVATE_KEY = cpi.ATTR_PRIVATE_KEY
+)
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "the basic auth user name",
+ ATTR_PASSWORD, "the basic auth password",
+ ATTR_IDENTITY_TOKEN, "the bearer token used for non-basic auth authorization",
+ ATTR_CERTIFICATE_AUTHORITY, "the certificate authority certificate used to verify certificates presented by the server",
+ ATTR_CERTIFICATE, "the certificate used to present to the server",
+ ATTR_PRIVATE_KEY, "the private key corresponding to the certificate",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher, `wget credential matcher
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func GetConsumerId(url string) cpi.ConsumerIdentity {
+ return hostpath.GetConsumerIdentity(CONSUMER_TYPE, url)
+}
diff --git a/api/credentials/builtin/wget/identity/identity_test.go b/api/credentials/builtin/wget/identity/identity_test.go
new file mode 100644
index 000000000..703b80526
--- /dev/null
+++ b/api/credentials/builtin/wget/identity/identity_test.go
@@ -0,0 +1,145 @@
+package identity_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/credentials/builtin/wget/identity"
+
+ "ocm.software/ocm/api/credentials"
+)
+
+var _ = Describe("wget credential management", func() {
+ Context("with path", func() {
+ pat := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a/b",
+ ID_PORT: "4711",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a/b",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("path prefix", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "b",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("longer prefix", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a/b/c",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing path", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a/b",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+
+ Expect(IdentityMatcher(id, nil, pat)).To(BeTrue()) // accept additional port as fallback
+ Expect(IdentityMatcher(id, id, pat)).To(BeFalse()) // but not to replace more general match
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PATHPREFIX: "a/b",
+ ID_PORT: "0815",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+
+ It("different host", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "other",
+ ID_PATHPREFIX: "a/b",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("no host", func() {
+ id := credentials.ConsumerIdentity{
+ ID_PATHPREFIX: "a/b",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(id, nil, pat)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, id, pat)).To(BeTrue())
+ })
+ })
+
+ Context("without path", func() {
+ pat := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PORT: "4711",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PORT: "4711",
+ ID_PATHPREFIX: "b",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ ID_HOSTNAME: "host",
+ ID_PORT: "0815",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ })
+})
diff --git a/pkg/contexts/credentials/builtin/wget/identity/suite_test.go b/api/credentials/builtin/wget/identity/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/builtin/wget/identity/suite_test.go
rename to api/credentials/builtin/wget/identity/suite_test.go
diff --git a/api/credentials/config/config_test.go b/api/credentials/config/config_test.go
new file mode 100644
index 000000000..2e7752452
--- /dev/null
+++ b/api/credentials/config/config_test.go
@@ -0,0 +1,209 @@
+package config_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/goutils/testutils"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/credentials"
+ localconfig "ocm.software/ocm/api/credentials/config"
+ "ocm.software/ocm/api/credentials/extensions/repositories/aliases"
+ "ocm.software/ocm/api/credentials/extensions/repositories/directcreds"
+ "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+var DefaultContext = credentials.New()
+
+var _ = Describe("generic credentials", func() {
+ props := common.Properties{
+ "user": "USER",
+ "password": "PASSWORD",
+ }
+
+ repospec := memory.NewRepositorySpec("test")
+ credspec := credentials.NewCredentialsSpec("cred", repospec)
+ direct := directcreds.NewRepositorySpec(props)
+
+ cfgconsumerdata := "{\"type\":\"credentials.config.ocm.software\",\"consumers\":[{\"identity\":{\"type\":\"oci\",\"url\":\"https://acme.com\"},\"credentials\":[{\"credentialsName\":\"cred\",\"repoName\":\"test\",\"type\":\"Memory\"}]}]}"
+ cfgrepodata := "{\"type\":\"credentials.config.ocm.software\",\"repositories\":[{\"repository\":{\"repoName\":\"test\",\"type\":\"Memory\"},\"credentials\":[{\"properties\":{\"password\":\"PASSWORD\",\"user\":\"USER\"},\"type\":\"Credentials\"}]}]}"
+ cfgaliasdata := "{\"type\":\"credentials.config.ocm.software\",\"aliases\":{\"alias\":{\"repository\":{\"repoName\":\"test\",\"type\":\"Memory\"},\"credentials\":[{\"properties\":{\"password\":\"PASSWORD\",\"user\":\"USER\"},\"type\":\"Credentials\"}]}}}"
+ _ = props
+
+ Context("serialize", func() {
+ It("serializes repository spec not in map", func() {
+ mapdata := "{\"repositories\":{\"repository\":{\"repoName\":\"test\",\"type\":\"Memory\"}}}"
+ type S struct {
+ Repositories localconfig.RepositorySpec `json:"repositories"`
+ }
+
+ rspec, err := credentials.ToGenericRepositorySpec(repospec)
+ Expect(err).To(Succeed())
+ s := &S{
+ Repositories: localconfig.RepositorySpec{Repository: *rspec},
+ }
+ data, err := json.Marshal(s)
+
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(mapdata)))
+ })
+
+ It("serializes repository spec map", func() {
+ mapdata := "{\"repositories\":{\"repo\":{\"repository\":{\"repoName\":\"test\",\"type\":\"Memory\"}}}}"
+ type S struct {
+ Repositories map[string]localconfig.RepositorySpec `json:"repositories"`
+ }
+
+ rspec, err := credentials.ToGenericRepositorySpec(repospec)
+ Expect(err).To(Succeed())
+ s := &S{
+ Repositories: map[string]localconfig.RepositorySpec{
+ "repo": {Repository: *rspec},
+ },
+ }
+ data, err := json.Marshal(s)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(mapdata)))
+ })
+ })
+
+ Context("composition", func() {
+ It("composes a config for consumers", func() {
+ consumerid := credentials.ConsumerIdentity{
+ "type": "oci",
+ "url": "https://acme.com",
+ }
+
+ cfg := localconfig.New()
+
+ cfg.AddConsumer(consumerid, credspec)
+
+ data, err := json.Marshal(cfg)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(cfgconsumerdata)))
+
+ cfg2 := &localconfig.Config{}
+ err = json.Unmarshal(data, cfg2)
+ Expect(err).To(Succeed())
+ Expect(cfg2).To(Equal(cfg))
+ })
+
+ It("composes a config for repositories", func() {
+ cfg := localconfig.New()
+
+ cfg.AddRepository(repospec, direct)
+
+ data, err := json.Marshal(cfg)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(cfgrepodata)))
+
+ cfg2 := &localconfig.Config{}
+ err = json.Unmarshal(data, cfg2)
+ Expect(err).To(Succeed())
+ Expect(cfg2).To(Equal(cfg))
+ })
+
+ It("composes a config for aliases", func() {
+ cfg := localconfig.New()
+
+ cfg.AddAlias("alias", repospec, direct)
+
+ data, err := json.Marshal(cfg)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(cfgaliasdata)))
+
+ cfg2 := &localconfig.Config{}
+ err = json.Unmarshal(data, cfg2)
+ Expect(err).To(Succeed())
+ Expect(cfg2).To(Equal(cfg))
+ })
+ })
+
+ Context("apply", func() {
+ var ctx credentials.Context
+
+ _ = ctx
+
+ BeforeEach(func() {
+ ctx = credentials.WithConfigs(config.New()).New()
+ })
+
+ It("applies a config for aliases", func() {
+ cfg := localconfig.New()
+ cfg.AddAlias("alias", repospec, direct)
+
+ ctx.ConfigContext().ApplyConfig(cfg, "testconfig")
+
+ spec := aliases.NewRepositorySpec("alias")
+
+ repo, err := ctx.RepositoryForSpec(spec)
+ Expect(err).To(Succeed())
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*memory.Repository"))
+ })
+
+ It("applies a config for consumers", func() {
+ cfg := localconfig.New()
+
+ consumer := credentials.ConsumerIdentity{
+ credentials.ID_TYPE: "mytype",
+ "host": "localhost",
+ }
+ props := common.Properties{"token": "mytoken"}
+ creds := directcreds.NewCredentials(props)
+ Expect(cfg.AddConsumer(consumer, creds)).To(Succeed())
+
+ data, err := runtime.DefaultYAMLEncoding.Marshal(cfg)
+ Expect(err).To(Succeed())
+ Expect(string(data)).To(testutils.StringEqualTrimmedWithContext(`
+consumers:
+- credentials:
+ - credentialsName: Credentials
+ properties:
+ token: mytoken
+ type: Credentials
+ identity:
+ host: localhost
+ type: mytype
+type: credentials.config.ocm.software
+`))
+
+ ctx.ConfigContext().ApplyConfig(cfg, "testconfig")
+
+ result, err := credentials.CredentialsForConsumer(ctx, consumer, credentials.CompleteMatch)
+ Expect(err).To(Succeed())
+
+ Expect(result.Properties()).To(Equal(props))
+ })
+
+ It("applies a config for consumers", func() {
+ props := common.Properties{"token": "mytoken"}
+ consumer := credentials.ConsumerIdentity{
+ credentials.ID_TYPE: "mytype",
+ "host": "localhost",
+ }
+ data := `
+type: credentials.config.ocm.software
+consumers:
+- credentials:
+ - type: Credentials
+ properties:
+ token: mytoken
+ identity:
+ host: localhost
+ type: mytype
+`
+ ctx.ConfigContext().ApplyData([]byte(data), nil, "testconfig")
+
+ result, err := credentials.CredentialsForConsumer(ctx, consumer, credentials.CompleteMatch)
+ Expect(err).To(Succeed())
+
+ Expect(result.Properties()).To(Equal(props))
+ })
+ })
+})
diff --git a/pkg/contexts/credentials/config/suite_test.go b/api/credentials/config/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/config/suite_test.go
rename to api/credentials/config/suite_test.go
diff --git a/api/credentials/config/type.go b/api/credentials/config/type.go
new file mode 100644
index 000000000..05f56f4ff
--- /dev/null
+++ b/api/credentials/config/type.go
@@ -0,0 +1,186 @@
+package config
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "credentials" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+// Config describes a configuration for the config context.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ // Consumers describe predefine logical cosumer specs mapped to credentials
+ // These will (potentially) be evaluated if access objects requiring credentials
+ // are provided by other modules (e.g. oci repo access) without
+ // specifying crednentials. Then this module can request credentials here by passing
+ // an appropriate consumer spec.
+ Consumers []ConsumerSpec `json:"consumers,omitempty"`
+ // Repositories describe preloaded credential repositories with potential credential chain
+ Repositories []RepositorySpec `json:"repositories,omitempty"`
+ // Aliases describe logical credential repositories mapped to implementing repositories
+ Aliases map[string]RepositorySpec `json:"aliases,omitempty"`
+}
+
+type ConsumerSpec struct {
+ Identity cpi.ConsumerIdentity `json:"identity"`
+ Credentials []cpi.GenericCredentialsSpec `json:"credentials"`
+}
+
+type RepositorySpec struct {
+ Repository cpi.GenericRepositorySpec `json:"repository"`
+ Credentials []cpi.GenericCredentialsSpec `json:"credentials,omitempty"`
+}
+
+// NewConfigSpec creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) MapCredentialsChain(creds ...cpi.CredentialsSpec) ([]cpi.GenericCredentialsSpec, error) {
+ var cgens []cpi.GenericCredentialsSpec
+ for _, c := range creds {
+ cgen, err := cpi.ToGenericCredentialsSpec(c)
+ if err != nil {
+ return nil, err
+ }
+ cgens = append(cgens, *cgen)
+ }
+ return cgens, nil
+}
+
+func (a *Config) AddConsumer(id cpi.ConsumerIdentity, creds ...cpi.CredentialsSpec) error {
+ cgens, err := a.MapCredentialsChain(creds...)
+ if err != nil {
+ return fmt.Errorf("failed to map credentials chain: %w", err)
+ }
+
+ spec := &ConsumerSpec{
+ Identity: id,
+ Credentials: cgens,
+ }
+ a.Consumers = append(a.Consumers, *spec)
+ return nil
+}
+
+func (a *Config) MapRepository(repo cpi.RepositorySpec, creds ...cpi.CredentialsSpec) (*RepositorySpec, error) {
+ rgen, err := cpi.ToGenericRepositorySpec(repo)
+ if err != nil {
+ return nil, err
+ }
+
+ cgens, err := a.MapCredentialsChain(creds...)
+ if err != nil {
+ return nil, err
+ }
+
+ return &RepositorySpec{
+ Repository: *rgen,
+ Credentials: cgens,
+ }, nil
+}
+
+func (a *Config) AddRepository(repo cpi.RepositorySpec, creds ...cpi.CredentialsSpec) error {
+ spec, err := a.MapRepository(repo, creds...)
+ if err != nil {
+ return fmt.Errorf("failed to map repository: %w", err)
+ }
+
+ a.Repositories = append(a.Repositories, *spec)
+
+ return nil
+}
+
+func (a *Config) AddAlias(name string, repo cpi.RepositorySpec, creds ...cpi.CredentialsSpec) error {
+ spec, err := a.MapRepository(repo, creds...)
+ if err != nil {
+ return fmt.Errorf("failed to map repository: %w", err)
+ }
+
+ if a.Aliases == nil {
+ a.Aliases = map[string]RepositorySpec{}
+ }
+ a.Aliases[name] = *spec
+ return nil
+}
+
+// --- begin apply ---
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ list := errors.ErrListf("applying config")
+ t, ok := target.(cpi.Context)
+ if !ok {
+ return cfgcpi.ErrNoContext(ConfigType)
+ }
+ for _, e := range a.Consumers {
+ t.SetCredentialsForConsumer(e.Identity, CredentialsChain(e.Credentials...))
+ }
+ // --- end apply ---
+ sub := errors.ErrListf("applying aliases")
+ for n, e := range a.Aliases {
+ sub.Add(t.SetAlias(n, &e.Repository, CredentialsChain(e.Credentials...)))
+ }
+ list.Add(sub.Result())
+ sub = errors.ErrListf("applying repositories")
+ for i, e := range a.Repositories {
+ _, err := t.RepositoryForSpec(&e.Repository, CredentialsChain(e.Credentials...))
+ sub.Add(errors.Wrapf(err, "repository entry %d", i))
+ }
+ list.Add(sub.Result())
+
+ return list.Result()
+}
+
+func CredentialsChain(creds ...cpi.GenericCredentialsSpec) cpi.CredentialsChain {
+ r := make([]cpi.CredentialsSource, len(creds))
+ for i := range creds {
+ r[i] = &creds[i]
+ }
+ return r
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define a list
+of arbitrary configuration specifications:
+
++ type: ` + ConfigType + ` + consumers: + - identity: + <name>: <value> + ... + credentials: + - <credential specification> + ... credential chain + repositories: + - repository: <repository specification> + credentials: + - <credential specification> + ... credential chain + aliases: + <name>: + repository: <repository specification> + credentials: + - <credential specification> + ... credential chain ++` diff --git a/api/credentials/const.go b/api/credentials/const.go new file mode 100644 index 000000000..88b5e8774 --- /dev/null +++ b/api/credentials/const.go @@ -0,0 +1,20 @@ +package credentials + +import ( + "ocm.software/ocm/api/credentials/internal" +) + +const ( + ID_TYPE = internal.ID_TYPE + + ATTR_TYPE = internal.ATTR_TYPE + ATTR_USERNAME = internal.ATTR_USERNAME + ATTR_PASSWORD = internal.ATTR_PASSWORD + ATTR_CERTIFICATE_AUTHORITY = internal.ATTR_CERTIFICATE_AUTHORITY + ATTR_CERTIFICATE = internal.ATTR_CERTIFICATE // PEM encoded + ATTR_PRIVATE_KEY = internal.ATTR_PRIVATE_KEY // PEM encoded + ATTR_SERVER_ADDRESS = internal.ATTR_SERVER_ADDRESS + ATTR_IDENTITY_TOKEN = internal.ATTR_IDENTITY_TOKEN + ATTR_REGISTRY_TOKEN = internal.ATTR_REGISTRY_TOKEN + ATTR_TOKEN = internal.ATTR_TOKEN +) diff --git a/pkg/contexts/credentials/cpi/README.md b/api/credentials/cpi/README.md similarity index 100% rename from pkg/contexts/credentials/cpi/README.md rename to api/credentials/cpi/README.md diff --git a/pkg/contexts/credentials/cpi/builtin.go b/api/credentials/cpi/builtin.go similarity index 88% rename from pkg/contexts/credentials/cpi/builtin.go rename to api/credentials/cpi/builtin.go index 3384e3dd2..93cf99123 100644 --- a/pkg/contexts/credentials/cpi/builtin.go +++ b/api/credentials/cpi/builtin.go @@ -1,7 +1,7 @@ package cpi import ( - "github.com/open-component-model/ocm/pkg/contexts/credentials/internal" + "ocm.software/ocm/api/credentials/internal" ) const AliasRepositoryType = internal.AliasRepositoryType diff --git a/api/credentials/cpi/const.go b/api/credentials/cpi/const.go new file mode 100644 index 000000000..a68cf53ec --- /dev/null +++ b/api/credentials/cpi/const.go @@ -0,0 +1,22 @@ +package cpi + +import ( + "ocm.software/ocm/api/credentials/internal" +) + +const ( + ID_TYPE = internal.ID_TYPE + + ATTR_TYPE = internal.ATTR_TYPE + ATTR_USERNAME = internal.ATTR_USERNAME + ATTR_EMAIL = internal.ATTR_EMAIL + ATTR_PASSWORD = internal.ATTR_PASSWORD + ATTR_SERVER_ADDRESS = internal.ATTR_SERVER_ADDRESS + ATTR_TOKEN = internal.ATTR_TOKEN + ATTR_IDENTITY_TOKEN = internal.ATTR_IDENTITY_TOKEN + ATTR_REGISTRY_TOKEN = internal.ATTR_REGISTRY_TOKEN + ATTR_KEY = internal.ATTR_KEY + ATTR_CERTIFICATE_AUTHORITY = internal.ATTR_CERTIFICATE_AUTHORITY + ATTR_CERTIFICATE = internal.ATTR_CERTIFICATE + ATTR_PRIVATE_KEY = internal.ATTR_PRIVATE_KEY +) diff --git a/api/credentials/cpi/interface.go b/api/credentials/cpi/interface.go new file mode 100644 index 000000000..f2870d8fe --- /dev/null +++ b/api/credentials/cpi/interface.go @@ -0,0 +1,128 @@ +package cpi + +// This is the Context Provider Interface for credential providers + +import ( + "ocm.software/ocm/api/credentials/internal" + "ocm.software/ocm/api/datacontext" + common "ocm.software/ocm/api/utils/misc" +) + +const ( + KIND_CREDENTIALS = internal.KIND_CREDENTIALS + KIND_REPOSITORY = internal.KIND_REPOSITORY +) + +const CONTEXT_TYPE = internal.CONTEXT_TYPE + +type ( + Context = internal.Context + ContextProvider = internal.ContextProvider + Repository = internal.Repository + RepositoryType = internal.RepositoryType + RepositoryTypeProvider = internal.RepositoryTypeProvider + RepositoryTypeScheme = internal.RepositoryTypeScheme + Credentials = internal.Credentials + CredentialsSource = internal.CredentialsSource + CredentialsChain = internal.CredentialsChain + CredentialsSpec = internal.CredentialsSpec + RepositorySpec = internal.RepositorySpec + GenericRepositorySpec = internal.GenericRepositorySpec + GenericCredentialsSpec = internal.GenericCredentialsSpec + DirectCredentials = internal.DirectCredentials + EvaluationContext = internal.EvaluationContext +) + +type ( + ConsumerIdentity = internal.ConsumerIdentity + ConsumerIdentityProvider = internal.ConsumerIdentityProvider + ProviderIdentity = internal.ProviderIdentity + ConsumerProvider = internal.ConsumerProvider + UsageContext = internal.UsageContext + StringUsageContext = internal.StringUsageContext + IdentityMatcher = internal.IdentityMatcher + IdentityMatcherInfo = internal.IdentityMatcherInfo + IdentityMatcherRegistry = internal.IdentityMatcherRegistry +) + +var DefaultContext = internal.DefaultContext + +func FromProvider(p ContextProvider) Context { + return internal.FromProvider(p) +} + +func New(m ...datacontext.BuilderMode) Context { + return internal.Builder{}.New(m...) +} + +func NewConsumerIdentity(typ string, attrs ...string) ConsumerIdentity { + return internal.NewConsumerIdentity(typ, attrs...) +} + +func NewGenericCredentialsSpec(name string, repospec *GenericRepositorySpec) *GenericCredentialsSpec { + return internal.NewGenericCredentialsSpec(name, repospec) +} + +func NewCredentialsSpec(name string, repospec RepositorySpec) CredentialsSpec { + return internal.NewCredentialsSpec(name, repospec) +} + +func ToGenericCredentialsSpec(spec CredentialsSpec) (*GenericCredentialsSpec, error) { + return internal.ToGenericCredentialsSpec(spec) +} + +func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error) { + return internal.ToGenericRepositorySpec(spec) +} + +func RegisterStandardIdentityMatcher(typ string, matcher IdentityMatcher, desc string) { + internal.StandardIdentityMatchers.Register(typ, matcher, desc) +} + +func RegisterStandardIdentity(typ string, matcher IdentityMatcher, desc string, attrs string) { + internal.StandardIdentityMatchers.Register(typ, matcher, desc, attrs) +} + +func NewCredentials(props common.Properties) Credentials { + return internal.NewCredentials(props) +} + +func ErrUnknownCredentials(name string) error { + return internal.ErrUnknownCredentials(name) +} + +func ErrUnknownRepository(kind, name string) error { + return internal.ErrUnknownRepository(kind, name) +} + +func CredentialsForConsumer(ctx ContextProvider, id ConsumerIdentity, matchers ...IdentityMatcher) (Credentials, error) { + return internal.CredentialsForConsumer(ctx, id, false, matchers...) +} + +func RequiredCredentialsForConsumer(ctx ContextProvider, id ConsumerIdentity, matchers ...IdentityMatcher) (Credentials, error) { + return internal.CredentialsForConsumer(ctx, id, true, matchers...) +} + +func GetCredentialsForConsumer(ctx Context, ectx EvaluationContext, identity ConsumerIdentity, matchers ...IdentityMatcher) (CredentialsSource, error) { + return internal.GetCredentialsForConsumer(ctx, ectx, identity, matchers...) +} + +func GetEvaluationContextFor[T any](ectx EvaluationContext) T { + return internal.GetEvaluationContextFor[T](ectx) +} + +func SetEvaluationContextFor(ectx EvaluationContext, e any) { + internal.SetEvaluationContextFor(ectx, e) +} + +var ( + CompleteMatch = internal.CompleteMatch + NoMatch = internal.NoMatch + PartialMatch = internal.PartialMatch +) + +// provide context interface for other files to avoid diffs in imports. +var ( + newStrictRepositoryTypeScheme = internal.NewStrictRepositoryTypeScheme + defaultRepositoryTypeScheme = internal.DefaultRepositoryTypeScheme +) diff --git a/api/credentials/cpi/repotypes.go b/api/credentials/cpi/repotypes.go new file mode 100644 index 000000000..673998c36 --- /dev/null +++ b/api/credentials/cpi/repotypes.go @@ -0,0 +1,49 @@ +package cpi + +// this file is identical for contexts oci and credentials and similar for +// ocm. + +import ( + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/api/utils/runtime/descriptivetype" +) + +type RepositoryTypeVersionScheme = runtime.TypeVersionScheme[RepositorySpec, RepositoryType] + +func NewRepositoryTypeVersionScheme(kind string) RepositoryTypeVersionScheme { + return runtime.NewTypeVersionScheme[RepositorySpec, RepositoryType](kind, newStrictRepositoryTypeScheme()) +} + +func RegisterRepositoryType(rtype RepositoryType) { + defaultRepositoryTypeScheme.Register(rtype) +} + +func RegisterRepositoryTypeVersions(s RepositoryTypeVersionScheme) { + defaultRepositoryTypeScheme.AddKnownTypes(s) +} + +//////////////////////////////////////////////////////////////////////////////// + +func NewRepositoryType[I RepositorySpec](name string, opts ...RepositoryOption) RepositoryType { + return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectType[RepositorySpec, I](name), opts...) +} + +func NewRepositoryTypeByConverter[I RepositorySpec, V runtime.TypedObject](name string, converter runtime.Converter[I, V], opts ...RepositoryOption) RepositoryType { + return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByConverter[RepositorySpec, I](name, converter), opts...) +} + +func NewRepositoryTypeByFormatVersion(name string, fmt runtime.FormatVersion[RepositorySpec], opts ...RepositoryOption) RepositoryType { + return descriptivetype.NewTypedObjectTypeObject(runtime.NewVersionedTypedObjectTypeByFormatVersion[RepositorySpec](name, fmt), opts...) +} + +//////////////////////////////////////////////////////////////////////////////// + +type RepositoryOption = descriptivetype.Option + +func WithDescription(v string) RepositoryOption { + return descriptivetype.WithDescription(v) +} + +func WithFormatSpec(v string) RepositoryOption { + return descriptivetype.WithFormatSpec(v) +} diff --git a/pkg/contexts/credentials/doc.go b/api/credentials/doc.go similarity index 100% rename from pkg/contexts/credentials/doc.go rename to api/credentials/doc.go diff --git a/api/credentials/extensions/repositories/aliases/cache.go b/api/credentials/extensions/repositories/aliases/cache.go new file mode 100644 index 000000000..102f9a79f --- /dev/null +++ b/api/credentials/extensions/repositories/aliases/cache.go @@ -0,0 +1,37 @@ +package aliases + +import ( + "sync" + + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/datacontext" +) + +const ATTR_REPOS = "ocm.software/ocm/api/credentials/extensions/repositories/aliases" + +type Repositories struct { + sync.RWMutex + repos map[string]*Repository +} + +func newRepositories(datacontext.Context) interface{} { + return &Repositories{ + repos: map[string]*Repository{}, + } +} + +func (c *Repositories) GetRepository(name string) *Repository { + c.RLock() + defer c.RUnlock() + return c.repos[name] +} + +func (c *Repositories) Set(name string, spec cpi.RepositorySpec, creds cpi.CredentialsSource) { + c.Lock() + defer c.Unlock() + c.repos[name] = &Repository{ + name: name, + spec: spec, + creds: creds, + } +} diff --git a/api/credentials/extensions/repositories/aliases/repo_test.go b/api/credentials/extensions/repositories/aliases/repo_test.go new file mode 100644 index 000000000..36bbe8b2c --- /dev/null +++ b/api/credentials/extensions/repositories/aliases/repo_test.go @@ -0,0 +1,70 @@ +package aliases_test + +import ( + "encoding/json" + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/credentials" + local "ocm.software/ocm/api/credentials/extensions/repositories/aliases" + common "ocm.software/ocm/api/utils/misc" +) + +var DefaultContext = credentials.New() + +var _ = Describe("alias credentials", func() { + props := common.Properties{ + "user": "USER", + "password": "PASSWORD", + } + + memorydata := "{\"type\":\"Memory\",\"repoName\":\"myrepo\"}" + specdata := "{\"type\":\"Alias\",\"alias\":\"test\"}" + + It("serializes repo spec", func() { + spec := local.NewRepositorySpec("test") + data, err := json.Marshal(spec) + Expect(err).To(Succeed()) + Expect(data).To(Equal([]byte(specdata))) + }) + It("deserializes repo spec", func() { + spec, err := DefaultContext.RepositorySpecForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + Expect(reflect.TypeOf(spec).String()).To(Equal("*aliases.RepositorySpec")) + Expect(spec.(*local.RepositorySpec).Alias).To(Equal("test")) + }) + + It("resolves repository", func() { + memoryspec, err := credentials.NewGenericRepositorySpec([]byte(memorydata), nil) + Expect(err).To(Succeed()) + + err = DefaultContext.SetAlias("test", memoryspec) + Expect(err).To(Succeed()) + + repo, err := DefaultContext.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + Expect(reflect.TypeOf(repo).String()).To(Equal("*memory.Repository")) + }) + + It("sets and retrieves credentials", func() { + memoryspec, err := credentials.NewGenericRepositorySpec([]byte(memorydata), nil) + Expect(err).To(Succeed()) + + err = DefaultContext.SetAlias("test", memoryspec) + Expect(err).To(Succeed()) + + repo, err := DefaultContext.RepositoryForConfig([]byte(memorydata), nil) + Expect(err).To(Succeed()) + + _, err = repo.WriteCredentials("bibo", credentials.NewCredentials(props)) + Expect(err).To(Succeed()) + + credspec := credentials.NewCredentialsSpec("bibo", local.NewRepositorySpec("test")) + + creds, err := DefaultContext.CredentialsForSpec(credspec) + Expect(err).To(Succeed()) + Expect(creds.Properties()).To(Equal(props)) + }) +}) diff --git a/api/credentials/extensions/repositories/aliases/repository.go b/api/credentials/extensions/repositories/aliases/repository.go new file mode 100644 index 000000000..d2df5d6ad --- /dev/null +++ b/api/credentials/extensions/repositories/aliases/repository.go @@ -0,0 +1,45 @@ +package aliases + +import ( + "sync" + + "ocm.software/ocm/api/credentials/cpi" +) + +type Repository struct { + sync.Mutex + name string + spec cpi.RepositorySpec + creds cpi.CredentialsSource + repo cpi.Repository +} + +func (a *Repository) GetRepository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) { + a.Lock() + defer a.Unlock() + if a.repo != nil { + return a.repo, nil + } + + src := cpi.CredentialsChain{} + if a.creds != nil { + src = append(src, a.creds) + } + if creds != nil { + src = append(src, creds) + } + repo, err := ctx.RepositoryForSpec(a.spec, src...) + if err != nil { + return nil, err + } + a.repo = repo + return repo, nil +} + +func NewRepository(name string, spec cpi.RepositorySpec, creds cpi.Credentials) *Repository { + return &Repository{ + name: name, + spec: spec, + creds: creds, + } +} diff --git a/pkg/contexts/credentials/repositories/aliases/suite_test.go b/api/credentials/extensions/repositories/aliases/suite_test.go similarity index 100% rename from pkg/contexts/credentials/repositories/aliases/suite_test.go rename to api/credentials/extensions/repositories/aliases/suite_test.go diff --git a/api/credentials/extensions/repositories/aliases/type.go b/api/credentials/extensions/repositories/aliases/type.go new file mode 100644 index 000000000..c0fb7989e --- /dev/null +++ b/api/credentials/extensions/repositories/aliases/type.go @@ -0,0 +1,59 @@ +package aliases + +import ( + "fmt" + + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + Type = cpi.AliasRepositoryType + TypeV1 = Type + runtime.VersionSeparator + "v1" +) + +func init() { + cpi.RegisterRepositoryType(cpi.NewAliasRegistry(cpi.NewRepositoryType[*RepositorySpec](Type), setAlias)) + cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1)) +} + +func setAlias(ctx cpi.Context, name string, spec cpi.RepositorySpec, creds cpi.CredentialsSource) error { + r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories) + repos, ok := r.(*Repositories) + if !ok { + return fmt.Errorf("failed to assert type %T to Repositories", r) + } + repos.Set(name, spec, creds) + return nil +} + +// RepositorySpec describes a memory based repository interface. +type RepositorySpec struct { + runtime.ObjectVersionedType `json:",inline"` + Alias string `json:"alias"` +} + +// NewRepositorySpec creates a new memory RepositorySpec. +func NewRepositorySpec(name string) *RepositorySpec { + return &RepositorySpec{ + ObjectVersionedType: runtime.NewVersionedTypedObject(Type), + Alias: name, + } +} + +func (a *RepositorySpec) GetType() string { + return Type +} + +func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) { + r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories) + repos, ok := r.(*Repositories) + if !ok { + return nil, fmt.Errorf("failed to assert type %T to Repositories", r) + } + alias := repos.GetRepository(a.Alias) + if alias == nil { + return nil, cpi.ErrUnknownRepository(Type, a.Alias) + } + return alias.GetRepository(ctx, creds) +} diff --git a/api/credentials/extensions/repositories/directcreds/a_usage.go b/api/credentials/extensions/repositories/directcreds/a_usage.go new file mode 100644 index 000000000..cd749a3c5 --- /dev/null +++ b/api/credentials/extensions/repositories/directcreds/a_usage.go @@ -0,0 +1,14 @@ +package directcreds + +import ( + "ocm.software/ocm/api/utils/listformat" +) + +var usage = ` +This repository type can be used to specify a single inline credential +set. The default name is the empty string or
` + Type + `
.`
+
+var format = `The repository specification supports the following fields:
+` + listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ "properties", "*map[string]string*: direct credential fields",
+})
diff --git a/api/credentials/extensions/repositories/directcreds/credentials.go b/api/credentials/extensions/repositories/directcreds/credentials.go
new file mode 100644
index 000000000..460545563
--- /dev/null
+++ b/api/credentials/extensions/repositories/directcreds/credentials.go
@@ -0,0 +1,10 @@
+package directcreds
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+func NewCredentials(props common.Properties) cpi.CredentialsSpec {
+ return cpi.NewCredentialsSpec(Type, NewRepositorySpec(props))
+}
diff --git a/api/credentials/extensions/repositories/directcreds/repo_test.go b/api/credentials/extensions/repositories/directcreds/repo_test.go
new file mode 100644
index 000000000..be1e07ec9
--- /dev/null
+++ b/api/credentials/extensions/repositories/directcreds/repo_test.go
@@ -0,0 +1,43 @@
+package directcreds_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/extensions/repositories/directcreds"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+var DefaultContext = credentials.New()
+
+var _ = Describe("direct credentials", func() {
+ props := common.Properties{
+ "user": "USER",
+ "password": "PASSWORD",
+ }
+ propsdata := "{\"type\":\"Credentials\",\"properties\":{\"password\":\"PASSWORD\",\"user\":\"USER\"}}"
+
+ It("serializes credentials spec", func() {
+ spec := directcreds.NewRepositorySpec(props)
+ data, err := json.Marshal(spec)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(propsdata)))
+ })
+ It("deserializes credentials spec", func() {
+ spec, err := DefaultContext.RepositoryForConfig([]byte(propsdata), nil)
+ Expect(err).To(Succeed())
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*directcreds.Repository"))
+ })
+
+ It("resolved direct credentials", func() {
+ spec := directcreds.NewCredentials(props)
+
+ creds, err := DefaultContext.CredentialsForSpec(spec)
+ Expect(err).To(Succeed())
+ Expect(creds.Properties()).To(Equal(props))
+ })
+})
diff --git a/api/credentials/extensions/repositories/directcreds/repository.go b/api/credentials/extensions/repositories/directcreds/repository.go
new file mode 100644
index 000000000..915683d3c
--- /dev/null
+++ b/api/credentials/extensions/repositories/directcreds/repository.go
@@ -0,0 +1,34 @@
+package directcreds
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+)
+
+type Repository struct {
+ Credentials cpi.Credentials
+}
+
+func NewRepository(creds cpi.Credentials) cpi.Repository {
+ return &Repository{
+ Credentials: creds,
+ }
+}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ return name == Type, nil
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ if name != Type && name != "" {
+ return nil, cpi.ErrUnknownCredentials(name)
+ }
+ return r.Credentials, nil
+}
+
+func (r *Repository) WriteCredentials(name string, creds cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported(cpi.KIND_CREDENTIALS, "write", "constant credential")
+}
+
+var _ cpi.Repository = &Repository{}
diff --git a/pkg/contexts/credentials/repositories/directcreds/suite_test.go b/api/credentials/extensions/repositories/directcreds/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/directcreds/suite_test.go
rename to api/credentials/extensions/repositories/directcreds/suite_test.go
diff --git a/api/credentials/extensions/repositories/directcreds/type.go b/api/credentials/extensions/repositories/directcreds/type.go
new file mode 100644
index 000000000..1c36963df
--- /dev/null
+++ b/api/credentials/extensions/repositories/directcreds/type.go
@@ -0,0 +1,56 @@
+package directcreds
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "Credentials"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1, cpi.WithDescription(usage), cpi.WithFormatSpec(format)))
+}
+
+// RepositorySpec describes a repository interface for single direct credentials.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Properties common.Properties `json:"properties"`
+}
+
+var (
+ _ cpi.RepositorySpec = &RepositorySpec{}
+ _ cpi.CredentialsSpec = &RepositorySpec{}
+)
+
+// NewRepositorySpec creates a new RepositorySpec.
+func NewRepositorySpec(credentials common.Properties) *RepositorySpec {
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ Properties: credentials,
+ }
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) {
+ return NewRepository(cpi.NewCredentials(a.Properties)), nil
+}
+
+func (a *RepositorySpec) Credentials(context cpi.Context, source ...cpi.CredentialsSource) (cpi.Credentials, error) {
+ return cpi.NewCredentials(a.Properties), nil
+}
+
+func (a *RepositorySpec) GetCredentialsName() string {
+ return ""
+}
+
+func (a *RepositorySpec) GetRepositorySpec(context cpi.Context) cpi.RepositorySpec {
+ return a
+}
diff --git a/api/credentials/extensions/repositories/dockerconfig/a_usage.go b/api/credentials/extensions/repositories/dockerconfig/a_usage.go
new file mode 100644
index 000000000..d445b9b97
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/a_usage.go
@@ -0,0 +1,19 @@
+package dockerconfig
+
+import (
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+var usage = `
+This repository type can be used to access credentials stored in a file
+following the docker config json format. It take into account the
+credentials helper section, also. If enabled, the described
+credentials will be automatically assigned to appropriate consumer ids.
+`
+
+var format = `The repository specification supports the following fields:
+` + listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ "dockerConfigFile", "*string*: the file path to a docker config file",
+ "dockerConfig", "*json*: an embedded docker config json",
+ "propagateConsumerIdentity", "*bool*(optional): enable consumer id propagation",
+})
diff --git a/api/credentials/extensions/repositories/dockerconfig/cache.go b/api/credentials/extensions/repositories/dockerconfig/cache.go
new file mode 100644
index 000000000..c7a44ddfb
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/cache.go
@@ -0,0 +1,40 @@
+package dockerconfig
+
+import (
+ "sync"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/datacontext"
+)
+
+const ATTR_REPOS = "ocm.software/ocm/api/credentials/extensions/repositories/dockerconfig"
+
+type Repositories struct {
+ lock sync.Mutex
+ repos map[string]*Repository
+}
+
+func newRepositories(datacontext.Context) interface{} {
+ return &Repositories{
+ repos: map[string]*Repository{},
+ }
+}
+
+func (r *Repositories) GetRepository(ctx cpi.Context, name string, data []byte, propagate bool) (*Repository, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ var (
+ err error = nil
+ repo *Repository
+ )
+ if name != "" {
+ repo = r.repos[name]
+ }
+ if repo == nil {
+ repo, err = NewRepository(ctx, name, data, propagate)
+ if err == nil {
+ r.repos[name] = repo
+ }
+ }
+ return repo, err
+}
diff --git a/api/credentials/extensions/repositories/dockerconfig/credentials.go b/api/credentials/extensions/repositories/dockerconfig/credentials.go
new file mode 100644
index 000000000..92d966f88
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/credentials.go
@@ -0,0 +1,67 @@
+package dockerconfig
+
+import (
+ "github.com/docker/cli/cli/config/configfile"
+ dockercred "github.com/docker/cli/cli/config/credentials"
+ "github.com/docker/cli/cli/config/types"
+ "github.com/mandelsoft/goutils/set"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Credentials struct {
+ config *configfile.ConfigFile
+ name string
+ store dockercred.Store
+}
+
+var _ cpi.Credentials = (*Credentials)(nil)
+
+// NewCredentials describes a default getter method for a authentication method.
+func NewCredentials(cfg *configfile.ConfigFile, name string, store dockercred.Store) cpi.Credentials {
+ return &Credentials{
+ config: cfg,
+ name: name,
+ store: store,
+ }
+}
+
+func (c *Credentials) get() common.Properties {
+ auth, err := c.config.GetAuthConfig(c.name)
+ if err != nil {
+ return common.Properties{}
+ }
+ return newCredentials(auth).Properties()
+}
+
+func (c *Credentials) Credentials(context cpi.Context, source ...cpi.CredentialsSource) (cpi.Credentials, error) {
+ var auth types.AuthConfig
+ var err error
+ if c.store == nil {
+ auth, err = c.config.GetAuthConfig(c.name)
+ } else {
+ auth, err = c.store.Get(c.name)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return newCredentials(auth), nil
+}
+
+func (c *Credentials) ExistsProperty(name string) bool {
+ _, ok := c.get()[name]
+ return ok
+}
+
+func (c *Credentials) GetProperty(name string) string {
+ return c.get()[name]
+}
+
+func (c *Credentials) PropertyNames() set.Set[string] {
+ return c.get().Names()
+}
+
+func (c *Credentials) Properties() common.Properties {
+ return c.get()
+}
diff --git a/api/credentials/extensions/repositories/dockerconfig/default.go b/api/credentials/extensions/repositories/dockerconfig/default.go
new file mode 100644
index 000000000..49d7deeca
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/default.go
@@ -0,0 +1,32 @@
+package dockerconfig
+
+import (
+ dockercli "github.com/docker/cli/cli/config"
+ "github.com/mandelsoft/filepath/pkg/filepath"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/config"
+ credcfg "ocm.software/ocm/api/credentials/config"
+ "ocm.software/ocm/api/ocm/ocmutils/defaultconfigregistry"
+)
+
+func init() {
+ defaultconfigregistry.RegisterDefaultConfigHandler(DefaultConfigHandler, desc)
+}
+
+func DefaultConfigHandler(cfg config.Context) (string, config.Config, error) {
+ // use docker config as default config for ocm cli
+ d := filepath.Join(dockercli.Dir(), dockercli.ConfigFileName)
+ if ok, err := vfs.FileExists(osfs.New(), d); ok && err == nil {
+ ccfg := credcfg.New()
+ ccfg.AddRepository(NewRepositorySpec(d, true))
+ return d, ccfg, nil
+ }
+ return "", nil, nil
+}
+
+var desc = `
+The docker configuration file at ~/.docker/config.json
is
+read to feed in the configured credentials for OCI registries.
+`
diff --git a/api/credentials/extensions/repositories/dockerconfig/logging.go b/api/credentials/extensions/repositories/dockerconfig/logging.go
new file mode 100644
index 000000000..a5660b36d
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/logging.go
@@ -0,0 +1,7 @@
+package dockerconfig
+
+import (
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var REALM = ocmlog.DefineSubRealm("docker config handling as credential repository", "credentials/dockerconfig")
diff --git a/api/credentials/extensions/repositories/dockerconfig/provider.go b/api/credentials/extensions/repositories/dockerconfig/provider.go
new file mode 100644
index 000000000..f818d8b12
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/provider.go
@@ -0,0 +1,87 @@
+package dockerconfig
+
+import (
+ "github.com/docker/cli/cli/config/configfile"
+ dockercred "github.com/docker/cli/cli/config/credentials"
+
+ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils"
+)
+
+const PROVIDER = "ocm.software/credentialprovider/" + Type
+
+type ConsumerProvider struct {
+ cfg *configfile.ConfigFile
+}
+
+var _ cpi.ConsumerProvider = (*ConsumerProvider)(nil)
+
+func (p *ConsumerProvider) Unregister(id cpi.ProviderIdentity) {
+}
+
+func (p *ConsumerProvider) Match(ectx cpi.EvaluationContext, req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ return p.get(req, cur, m)
+}
+
+func (p *ConsumerProvider) Get(req cpi.ConsumerIdentity) (cpi.CredentialsSource, bool) {
+ creds, _ := p.get(req, nil, cpi.CompleteMatch)
+ return creds, creds != nil
+}
+
+func (p *ConsumerProvider) get(req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ cfg := p.cfg
+ all := cfg.GetAuthConfigs()
+ defaultStore := dockercred.DetectDefaultStore(cfg.CredentialsStore)
+ var store dockercred.Store
+ if defaultStore != "" {
+ store = dockercred.NewNativeStore(cfg, defaultStore)
+ }
+
+ var creds cpi.CredentialsSource
+
+ for h, a := range all {
+ hostname, port, _ := utils.SplitLocator(dockercred.ConvertToHostname(h))
+ if hostname == "index.docker.io" {
+ hostname = "docker.io"
+ }
+ attrs := []string{identity.ID_HOSTNAME, hostname}
+ if port != "" {
+ attrs = append(attrs, identity.ID_PORT, port)
+ }
+ id := cpi.NewConsumerIdentity(identity.CONSUMER_TYPE, attrs...)
+ if m(req, cur, id) {
+ if IsEmptyAuthConfig(a) {
+ store := store
+ for hh, helper := range cfg.CredentialHelpers {
+ if hh == h {
+ store = dockercred.NewNativeStore(cfg, helper)
+ break
+ }
+ }
+ if store == nil {
+ continue
+ }
+ creds = NewCredentials(cfg, h, store)
+ } else {
+ creds = newCredentials(a)
+ }
+ cur = id
+ }
+ }
+ for h, helper := range cfg.CredentialHelpers {
+ hostname := dockercred.ConvertToHostname(h)
+ if hostname == "index.docker.io" {
+ hostname = "docker.io"
+ }
+ id := cpi.ConsumerIdentity{
+ cpi.ATTR_TYPE: identity.CONSUMER_TYPE,
+ identity.ID_HOSTNAME: hostname,
+ }
+ if m(req, cur, id) {
+ creds = NewCredentials(cfg, h, dockercred.NewNativeStore(cfg, helper))
+ cur = id
+ }
+ }
+ return creds, cur
+}
diff --git a/api/credentials/extensions/repositories/dockerconfig/repo_test.go b/api/credentials/extensions/repositories/dockerconfig/repo_test.go
new file mode 100644
index 000000000..92981365d
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/repo_test.go
@@ -0,0 +1,172 @@
+package dockerconfig_test
+
+import (
+ "encoding/json"
+ "os"
+ "reflect"
+ "runtime"
+ "time"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ local "ocm.software/ocm/api/credentials/extensions/repositories/dockerconfig"
+ "ocm.software/ocm/api/datacontext"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+var _ = Describe("docker config", func() {
+ props := common.Properties{
+ "username": "mandelsoft",
+ "password": "password",
+ "serverAddress": "https://index.docker.io/v1/",
+ }
+
+ props2 := common.Properties{
+ "username": "mandelsoft",
+ "password": "token",
+ "serverAddress": "https://ghcr.io",
+ }
+
+ var DefaultContext credentials.Context
+
+ BeforeEach(func() {
+ DefaultContext = credentials.New()
+ })
+
+ Context("file based", func() {
+ specdata := "{\"type\":\"DockerConfig\",\"dockerConfigFile\":\"testdata/dockerconfig.json\"}"
+ specdata2 := "{\"type\":\"DockerConfig\",\"dockerConfigFile\":\"testdata/dockerconfig.json\",\"propagateConsumerIdentity\":true}"
+
+ It("serializes repo spec", func() {
+ spec := local.NewRepositorySpec("testdata/dockerconfig.json")
+ data := Must(json.Marshal(spec))
+ Expect(data).To(Equal([]byte(specdata)))
+
+ spec = local.NewRepositorySpec("testdata/dockerconfig.json").WithConsumerPropagation(true)
+ data = Must(json.Marshal(spec))
+ Expect(data).To(Equal([]byte(specdata2)))
+ })
+
+ It("deserializes repo spec", func() {
+ spec := Must(DefaultContext.RepositorySpecForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*dockerconfig.RepositorySpec"))
+ Expect(spec.(*local.RepositorySpec).DockerConfigFile).To(Equal("testdata/dockerconfig.json"))
+ })
+
+ It("resolves repository", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*dockerconfig.Repository"))
+ })
+
+ It("retrieves credentials", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+
+ creds := Must(repo.LookupCredentials("index.docker.io"))
+ Expect(creds.Properties()).To(Equal(props))
+
+ creds = Must(repo.LookupCredentials("ghcr.io"))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+
+ It("propagates credentials to consumer identity", func() {
+ Must(DefaultContext.RepositoryForConfig([]byte(specdata2), nil))
+
+ creds := Must(credentials.CredentialsForConsumer(DefaultContext, credentials.ConsumerIdentity{
+ cpi.ATTR_TYPE: identity.CONSUMER_TYPE,
+ identity.ID_HOSTNAME: "ghcr.io",
+ }))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+ })
+
+ Context("inline data", func() {
+ specdata := "{\"type\":\"DockerConfig\",\"dockerConfig\":{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"bWFuZGVsc29mdDpwYXNzd29yZA==\"},\"https://ghcr.io\":{\"auth\":\"bWFuZGVsc29mdDp0b2tlbg==\"}},\"HttpHeaders\":{\"User-Agent\":\"Docker-Client/18.06.1-ce (linux)\"}},\"propagateConsumerIdentity\":true}"
+
+ It("serializes repo spec", func() {
+ configdata := Must(os.ReadFile("testdata/dockerconfig.json"))
+ spec := local.NewRepositorySpecForConfig(configdata).WithConsumerPropagation(true)
+ data := Must(json.Marshal(spec))
+
+ var (
+ datajson map[string]interface{}
+ specjson map[string]interface{}
+ )
+ // Comparing the bytes might be problematic as the order of the JSON objects within the config file might change
+ // during Marshaling
+ MustBeSuccessful(json.Unmarshal([]byte(specdata), &specjson))
+ MustBeSuccessful(json.Unmarshal(data, &datajson))
+ Expect(datajson).To(Equal(specjson))
+ })
+
+ It("deserializes repo spec", func() {
+ spec := Must(DefaultContext.RepositorySpecForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*dockerconfig.RepositorySpec"))
+ configdata := Must(os.ReadFile("testdata/dockerconfig.json"))
+ var (
+ configdatajson map[string]interface{}
+ dockerconfigjson map[string]interface{}
+ )
+ // Comparing the bytes might be problematic as the order of the JSON objects within the config file might change
+ // during Marshaling
+ MustBeSuccessful(json.Unmarshal(configdata, &configdatajson))
+ MustBeSuccessful(json.Unmarshal(spec.(*local.RepositorySpec).DockerConfig, &dockerconfigjson))
+ Expect(dockerconfigjson).To(Equal(configdatajson))
+ })
+
+ It("resolves repository", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*dockerconfig.Repository"))
+ })
+
+ It("retrieves credentials", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+
+ creds := Must(repo.LookupCredentials("index.docker.io"))
+ Expect(creds.Properties()).To(Equal(props))
+
+ creds = Must(repo.LookupCredentials("ghcr.io"))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+
+ It("propagates credentials to consumer identity", func() {
+ Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+
+ creds := Must(credentials.CredentialsForConsumer(DefaultContext, credentials.ConsumerIdentity{
+ cpi.ATTR_TYPE: identity.CONSUMER_TYPE,
+ identity.ID_HOSTNAME: "ghcr.io",
+ }))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+ })
+
+ Context("ref handling", func() {
+ specdata := "{\"type\":\"DockerConfig\",\"dockerConfigFile\":\"testdata/dockerconfig.json\",\"propagateConsumerIdentity\":true}"
+
+ It("can access the default context", func() {
+ ctx := credentials.New()
+
+ r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx)
+ Expect(r).NotTo(BeNil())
+
+ Must(ctx.RepositoryForConfig([]byte(specdata), nil))
+
+ runtime.GC()
+ time.Sleep(time.Second)
+ ctx.GetType()
+ Expect(r.Get()).To(BeNil())
+
+ Expect(datacontext.GetContextRefCount(ctx)).To(Equal(1))
+ ctx = nil
+ runtime.GC()
+ time.Sleep(time.Second)
+
+ Expect(r.Get()).To(ContainElement(ContainSubstring(credentials.CONTEXT_TYPE)))
+ })
+ })
+})
diff --git a/api/credentials/extensions/repositories/dockerconfig/repository.go b/api/credentials/extensions/repositories/dockerconfig/repository.go
new file mode 100644
index 000000000..e82fc5548
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/repository.go
@@ -0,0 +1,143 @@
+package dockerconfig
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/docker/cli/cli/config"
+ "github.com/docker/cli/cli/config/configfile"
+ "github.com/docker/cli/cli/config/types"
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+type Repository struct {
+ lock sync.RWMutex
+ ctx cpi.Context
+ propagate bool
+ path string
+ data []byte
+ config *configfile.ConfigFile
+}
+
+func NewRepository(ctx cpi.Context, path string, data []byte, propagate bool) (*Repository, error) {
+ r := &Repository{
+ ctx: datacontext.InternalContextRef(ctx),
+ propagate: propagate,
+ path: path,
+ data: data,
+ }
+ if path != "" && len(data) > 0 {
+ return nil, fmt.Errorf("only config file or config data possible")
+ }
+ err := r.Read(true)
+ return r, err
+}
+
+var _ cpi.Repository = &Repository{}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ err := r.Read(false)
+ if err != nil {
+ return false, err
+ }
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ _, err = r.config.GetAuthConfig(name)
+ return err != nil, err
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ err := r.Read(false)
+ if err != nil {
+ return nil, err
+ }
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ auth, err := r.config.GetAuthConfig(name)
+ if err != nil {
+ return nil, err
+ }
+ return newCredentials(auth), nil
+}
+
+func (r *Repository) WriteCredentials(name string, creds cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported("write", "credentials", Type)
+}
+
+func (r *Repository) Read(force bool) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ if !force && r.config != nil {
+ return nil
+ }
+ var (
+ data []byte
+ err error
+ id runtimefinalizer.ObjectIdentity
+ )
+ if r.path != "" {
+ path, err := utils.ResolvePath(r.path)
+ if err != nil {
+ return errors.Wrapf(err, "cannot resolve path %q", r.path)
+ }
+ data, err = os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("failed to read file '%s': %w", path, err)
+ }
+ id = cpi.ProviderIdentity(PROVIDER + "/" + path)
+ } else if len(r.data) > 0 {
+ data = r.data
+ id = runtimefinalizer.NewObjectIdentity(PROVIDER)
+ }
+
+ cfg, err := config.LoadFromReader(bytes.NewBuffer(data))
+ if err != nil {
+ return fmt.Errorf("failed to load config: %w", err)
+ }
+ if r.propagate {
+ r.ctx.RegisterConsumerProvider(id, &ConsumerProvider{cfg})
+ }
+ r.config = cfg
+ return nil
+}
+
+func newCredentials(auth types.AuthConfig) cpi.Credentials {
+ props := common.Properties{
+ cpi.ATTR_USERNAME: norm(auth.Username),
+ cpi.ATTR_PASSWORD: norm(auth.Password),
+ }
+ props.SetNonEmptyValue("auth", auth.Auth)
+ props.SetNonEmptyValue(cpi.ATTR_SERVER_ADDRESS, norm(auth.ServerAddress))
+ props.SetNonEmptyValue(cpi.ATTR_IDENTITY_TOKEN, norm(auth.IdentityToken))
+ props.SetNonEmptyValue(cpi.ATTR_REGISTRY_TOKEN, norm(auth.RegistryToken))
+ return cpi.NewCredentials(props)
+}
+
+func norm(s string) string {
+ for strings.HasSuffix(s, "\n") {
+ s = s[:len(s)-1]
+ }
+ return s
+}
+
+// IsEmptyAuthConfig validates if the resulting auth config contains credentials.
+func IsEmptyAuthConfig(auth types.AuthConfig) bool {
+ if len(auth.Auth) != 0 {
+ return false
+ }
+ if len(auth.Username) != 0 {
+ return false
+ }
+ return true
+}
diff --git a/pkg/contexts/credentials/repositories/dockerconfig/suite_test.go b/api/credentials/extensions/repositories/dockerconfig/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/dockerconfig/suite_test.go
rename to api/credentials/extensions/repositories/dockerconfig/suite_test.go
diff --git a/pkg/contexts/credentials/repositories/dockerconfig/testdata/dockerconfig.json b/api/credentials/extensions/repositories/dockerconfig/testdata/dockerconfig.json
similarity index 100%
rename from pkg/contexts/credentials/repositories/dockerconfig/testdata/dockerconfig.json
rename to api/credentials/extensions/repositories/dockerconfig/testdata/dockerconfig.json
diff --git a/api/credentials/extensions/repositories/dockerconfig/type.go b/api/credentials/extensions/repositories/dockerconfig/type.go
new file mode 100644
index 000000000..6ec2282a1
--- /dev/null
+++ b/api/credentials/extensions/repositories/dockerconfig/type.go
@@ -0,0 +1,76 @@
+package dockerconfig
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/mandelsoft/goutils/generics"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "DockerConfig"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1, cpi.WithDescription(usage), cpi.WithFormatSpec(format)))
+}
+
+// RepositorySpec describes a docker config based credential repository interface.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ DockerConfigFile string `json:"dockerConfigFile,omitempty"`
+ DockerConfig json.RawMessage `json:"dockerConfig,omitempty"`
+ PropgateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
+}
+
+func (s RepositorySpec) WithConsumerPropagation(propagate bool) *RepositorySpec {
+ s.PropgateConsumerIdentity = &propagate
+ return &s
+}
+
+// NewRepositorySpec creates a new memory RepositorySpec.
+func NewRepositorySpec(path string, prop ...bool) *RepositorySpec {
+ var p *bool
+ if len(prop) > 0 {
+ p = generics.Pointer(utils.Optional(prop...))
+ }
+ if path == "" {
+ path = "~/.docker/config.json"
+ }
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ DockerConfigFile: path,
+ PropgateConsumerIdentity: p,
+ }
+}
+
+func NewRepositorySpecForConfig(data []byte, prop ...bool) *RepositorySpec {
+ var p *bool
+ if len(prop) > 0 {
+ p = generics.Pointer(utils.Optional(prop...))
+ }
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ DockerConfig: data,
+ PropgateConsumerIdentity: p,
+ }
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) {
+ r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories)
+ repos, ok := r.(*Repositories)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to Repositories", r)
+ }
+ return repos.GetRepository(ctx, a.DockerConfigFile, a.DockerConfig, utils.AsBool(a.PropgateConsumerIdentity, true))
+}
diff --git a/pkg/contexts/credentials/repositories/gardenerconfig/README.md b/api/credentials/extensions/repositories/gardenerconfig/README.md
similarity index 100%
rename from pkg/contexts/credentials/repositories/gardenerconfig/README.md
rename to api/credentials/extensions/repositories/gardenerconfig/README.md
diff --git a/api/credentials/extensions/repositories/gardenerconfig/cache.go b/api/credentials/extensions/repositories/gardenerconfig/cache.go
new file mode 100644
index 000000000..93bb50c24
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/cache.go
@@ -0,0 +1,36 @@
+package gardenerconfig
+
+import (
+ "fmt"
+ "sync"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+ "ocm.software/ocm/api/datacontext"
+)
+
+const ATTR_REPOS = "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig"
+
+type Repositories struct {
+ lock sync.Mutex
+ repos map[string]*Repository
+}
+
+func newRepositories(datacontext.Context) interface{} {
+ return &Repositories{
+ repos: map[string]*Repository{},
+ }
+}
+
+func (r *Repositories) GetRepository(ctx cpi.Context, url string, configType gardenercfgcpi.ConfigType, cipher Cipher, key []byte, propagateConsumerIdentity bool) (*Repository, error) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ if _, ok := r.repos[url]; !ok {
+ repo, err := NewRepository(ctx, url, configType, cipher, key, propagateConsumerIdentity)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create repository: %w", err)
+ }
+ r.repos[url] = repo
+ }
+ return r.repos[url], nil
+}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/cpi/interface.go b/api/credentials/extensions/repositories/gardenerconfig/cpi/interface.go
new file mode 100644
index 000000000..f9270d21d
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/cpi/interface.go
@@ -0,0 +1,53 @@
+package cpi
+
+import (
+ "io"
+ "sync"
+
+ "ocm.software/ocm/api/credentials/cpi"
+)
+
+type ConfigType string
+
+const (
+ ContainerRegistry ConfigType = "container_registry"
+)
+
+type Credential interface {
+ Name() string
+ ConsumerIdentity() cpi.ConsumerIdentity
+ Properties() cpi.Credentials
+}
+
+type Handler interface {
+ ConfigType() ConfigType
+ ParseConfig(io.Reader) ([]Credential, error)
+}
+
+var (
+ handlers = map[ConfigType]Handler{}
+ lock sync.RWMutex
+)
+
+func RegisterHandler(h Handler) {
+ lock.Lock()
+ defer lock.Unlock()
+ handlers[h.ConfigType()] = h
+}
+
+func GetHandler(configType ConfigType) Handler {
+ lock.RLock()
+ defer lock.RUnlock()
+ return handlers[configType]
+}
+
+func GetHandlers() map[ConfigType]Handler {
+ lock.RLock()
+ defer lock.RUnlock()
+
+ m := map[ConfigType]Handler{}
+ for k, v := range handlers {
+ m[k] = v
+ }
+ return m
+}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/credentials.go b/api/credentials/extensions/repositories/gardenerconfig/credentials.go
new file mode 100644
index 000000000..6b12aa92e
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/credentials.go
@@ -0,0 +1,15 @@
+package gardenerconfig
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+)
+
+type credentialGetter struct {
+ getCredentials func() (cpi.Credentials, error)
+}
+
+var _ cpi.CredentialsSource = credentialGetter{}
+
+func (c credentialGetter) Credentials(ctx cpi.Context, cs ...cpi.CredentialsSource) (cpi.Credentials, error) {
+ return c.getCredentials()
+}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/credentials.go b/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/credentials.go
new file mode 100644
index 000000000..de0885b9a
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/credentials.go
@@ -0,0 +1,26 @@
+package container_registry
+
+import (
+ "ocm.software/ocm/api/credentials/cpi"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+)
+
+type credentials struct {
+ name string
+ consumerIdentity cpi.ConsumerIdentity
+ properties cpi.Credentials
+}
+
+func (c credentials) Name() string {
+ return c.name
+}
+
+func (c credentials) ConsumerIdentity() cpi.ConsumerIdentity {
+ return c.consumerIdentity
+}
+
+func (c credentials) Properties() cpi.Credentials {
+ return c.properties
+}
+
+var _ gardenercfgcpi.Credential = credentials{}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/handler.go b/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/handler.go
new file mode 100644
index 000000000..509975e16
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry/handler.go
@@ -0,0 +1,99 @@
+package container_registry
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "strings"
+
+ "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+func init() {
+ gardenercfgcpi.RegisterHandler(Handler{})
+}
+
+// config is the struct that describes the gardener config data structure.
+type config struct {
+ ContainerRegistry map[string]*containerRegistryCredentials `json:"container_registry"`
+}
+
+// containerRegistryCredentials describes the container registry credentials struct as defined by the gardener config.
+type containerRegistryCredentials struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ Privileges string `json:"privileges"`
+ Host string `json:"host,omitempty"`
+ ImageReferencePrefixes []string `json:"image_reference_prefixes,omitempty"`
+}
+
+type Handler struct{}
+
+func (h Handler) ConfigType() gardenercfgcpi.ConfigType {
+ return gardenercfgcpi.ContainerRegistry
+}
+
+func (h Handler) ParseConfig(configReader io.Reader) ([]gardenercfgcpi.Credential, error) {
+ config := &config{}
+ if err := json.NewDecoder(configReader).Decode(&config); err != nil {
+ return nil, fmt.Errorf("unable to unmarshal config: %w", err)
+ }
+
+ creds := []gardenercfgcpi.Credential{}
+ for credentialName, credential := range config.ContainerRegistry {
+ var (
+ scheme string
+ port string
+ )
+ if credential.Host != "" {
+ parsedHost, err := utils.ParseURL(credential.Host)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse host: %w", err)
+ }
+ scheme = parsedHost.Scheme
+ port = parsedHost.Port()
+ }
+
+ for _, imgRef := range credential.ImageReferencePrefixes {
+ parsedImgPrefix, err := utils.ParseURL(imgRef)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse image prefix: %w", err)
+ }
+ if parsedImgPrefix.Host == "index.docker.io" {
+ parsedImgPrefix.Host = "docker.io"
+ }
+
+ consumerIdentity := cpi.ConsumerIdentity{
+ cpi.ID_TYPE: identity.CONSUMER_TYPE,
+ hostpath.ID_HOSTNAME: parsedImgPrefix.Host,
+ hostpath.ID_PATHPREFIX: strings.Trim(parsedImgPrefix.Path, "/"),
+ }
+ consumerIdentity.SetNonEmptyValue(hostpath.ID_SCHEME, scheme)
+ consumerIdentity.SetNonEmptyValue(hostpath.ID_PORT, port)
+
+ c := credentials{
+ name: credentialName,
+ consumerIdentity: consumerIdentity,
+ properties: newCredentialsFromContainerRegistryCredentials(credential),
+ }
+
+ creds = append(creds, c)
+ }
+ }
+
+ return creds, nil
+}
+
+func newCredentialsFromContainerRegistryCredentials(auth *containerRegistryCredentials) cpi.Credentials {
+ props := common.Properties{
+ cpi.ATTR_USERNAME: auth.Username,
+ cpi.ATTR_PASSWORD: auth.Password,
+ }
+ props.SetNonEmptyValue(cpi.ATTR_SERVER_ADDRESS, auth.Host)
+ return cpi.NewCredentials(props)
+}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/identity/identity.go b/api/credentials/extensions/repositories/gardenerconfig/identity/identity.go
new file mode 100644
index 000000000..e5b18c4a0
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/identity/identity.go
@@ -0,0 +1,60 @@
+package identity
+
+import (
+ "fmt"
+ "strings"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/listformat"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const CONSUMER_TYPE = "Buildcredentials" + common.OCM_TYPE_GROUP_SUFFIX
+
+// used identity attributes.
+const (
+ ID_SCHEME = hostpath.ID_SCHEME
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+)
+
+// used credential properties.
+const (
+ ATTR_KEY = cpi.ATTR_KEY
+)
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_KEY, "secret key use to access the credential server",
+ })
+
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher, `Gardener config credential matcher
+
+It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
+the `+hostpath.IDENTITY_TYPE+`
type.`,
+ attrs)
+}
+
+func GetConsumerId(configURL string) (cpi.ConsumerIdentity, error) {
+ parsedURL, err := utils.ParseURL(configURL)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse url: %w", err)
+ }
+
+ id := cpi.NewConsumerIdentity(CONSUMER_TYPE)
+ id.SetNonEmptyValue(ID_HOSTNAME, parsedURL.Host)
+ id.SetNonEmptyValue(ID_SCHEME, parsedURL.Scheme)
+ id.SetNonEmptyValue(ID_PATHPREFIX, strings.Trim(parsedURL.Path, "/"))
+ id.SetNonEmptyValue(ID_PORT, parsedURL.Port())
+
+ return id, nil
+}
diff --git a/api/credentials/extensions/repositories/gardenerconfig/init.go b/api/credentials/extensions/repositories/gardenerconfig/init.go
new file mode 100644
index 000000000..e4a591675
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/init.go
@@ -0,0 +1,5 @@
+package gardenerconfig
+
+import (
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/handler/container_registry"
+)
diff --git a/api/credentials/extensions/repositories/gardenerconfig/repo_test.go b/api/credentials/extensions/repositories/gardenerconfig/repo_test.go
new file mode 100644
index 000000000..6f0998959
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/repo_test.go
@@ -0,0 +1,200 @@
+package gardenerconfig_test
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/vfs/pkg/memoryfs"
+
+ "ocm.software/ocm/api/credentials"
+ ociidentity "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ local "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/identity"
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/utils"
+)
+
+var _ = Describe("gardener config", func() {
+ containerRegistryCfg := `{
+ "container_registry": {
+ "test-credentials": {
+ "username": "abc",
+ "password": "123",
+ "image_reference_prefixes": [
+ "eu.gcr.io/test-project"
+ ]
+ }
+ }
+}`
+ encryptionKey := "abcdefghijklmnop"
+ encryptedContainerRegistryCfg := "Uz4mfePXFOUbjUEZnRrnG8zP2T7lRH6bR2rFHYgWDwZUXfW7D5wArwY4dsBACPVFNapF7kcM9z79+LvJXd2kNoIfvUyMOhrSDAyv4LtUqYSKBOoRH/aJMnXjmN9GQBCXSRSJs/Fu21AoDNo8fA9zYvvc7WxTldkYC/vHxLVNJu5j176e1QiaS9hwDjgNhgyUT3XUjHUyQ19PcRgwDglRLfiL4Cs/fYPPxdg4YZQdCnc="
+ expectedCreds := cpi.DirectCredentials{
+ cpi.ATTR_USERNAME: "abc",
+ cpi.ATTR_PASSWORD: "123",
+ }
+
+ repoSpecTemplate := `{"type":"GardenerConfig","url":"%s","configType":"container_registry","cipher":"%s","propagateConsumerIdentity":true}`
+
+ var defaultContext credentials.Context
+
+ BeforeEach(func() {
+ defaultContext = credentials.New()
+ })
+
+ It("serializes repo spec", func() {
+ const (
+ url = "http://localhost:8080/container_registry"
+ cipher = local.Plaintext
+ )
+ expectedSpec := fmt.Sprintf(repoSpecTemplate, url, cipher)
+
+ spec := local.NewRepositorySpec("http://localhost:8080/container_registry", "container_registry", local.Plaintext, true)
+ data, err := json.Marshal(spec)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(data).To(Equal([]byte(expectedSpec)))
+ })
+
+ It("deserializes repo spec", func() {
+ const (
+ url = "http://localhost:8080/container_registry"
+ cipher = local.Plaintext
+ )
+ specdata := fmt.Sprintf(repoSpecTemplate, url, cipher)
+
+ spec, err := defaultContext.RepositorySpecForConfig([]byte(specdata), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*gardenerconfig.RepositorySpec"))
+
+ parsedSpec := spec.(*local.RepositorySpec)
+ Expect(parsedSpec.URL).To(Equal(url))
+ Expect(parsedSpec.ConfigType).To(Equal(gardenercfgcpi.ContainerRegistry))
+ Expect(parsedSpec.Cipher).To(Equal(cipher))
+ })
+
+ It("resolves repository", func() {
+ const (
+ url = "http://localhost:8080/container_registry"
+ cipher = local.Plaintext
+ )
+ specdata := fmt.Sprintf(repoSpecTemplate, url, cipher)
+
+ repo, err := defaultContext.RepositoryForConfig([]byte(specdata), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(repo).ToNot(BeNil())
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*gardenerconfig.Repository"))
+ })
+
+ It("retrieves credentials from unencrypted server", func() {
+ svr := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ writer.WriteHeader(200)
+ _, err := writer.Write([]byte(containerRegistryCfg))
+ Expect(err).ToNot(HaveOccurred())
+ }))
+ defer svr.Close()
+
+ spec := fmt.Sprintf(repoSpecTemplate, svr.URL, local.Plaintext)
+
+ repo, err := defaultContext.RepositoryForConfig([]byte(spec), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(repo).ToNot(BeNil())
+
+ credentialsFromRepo, err := repo.LookupCredentials("test-credentials")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentialsFromRepo).To(Equal(expectedCreds))
+ })
+
+ It("propagates credentials with consumer ids in the context", func() {
+ expectedConsumerId := cpi.ConsumerIdentity{
+ cpi.ID_TYPE: ociidentity.CONSUMER_TYPE,
+ ociidentity.ID_HOSTNAME: "eu.gcr.io",
+ ociidentity.ID_PATHPREFIX: "test-project",
+ }
+
+ svr := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ writer.WriteHeader(200)
+ _, err := writer.Write([]byte(containerRegistryCfg))
+ Expect(err).ToNot(HaveOccurred())
+ }))
+ defer svr.Close()
+
+ spec := fmt.Sprintf(repoSpecTemplate, svr.URL, local.Plaintext)
+
+ repo, err := defaultContext.RepositoryForConfig([]byte(spec), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(repo).ToNot(BeNil())
+
+ credentialsFromCtx, err := credentials.CredentialsForConsumer(defaultContext, expectedConsumerId)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentialsFromCtx).To(Equal(expectedCreds))
+ })
+
+ It("retrieves credentials from encrypted server", func() {
+ svr := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ writer.WriteHeader(200)
+ data, err := base64.StdEncoding.DecodeString(encryptedContainerRegistryCfg)
+ Expect(err).ToNot(HaveOccurred())
+ _, err = writer.Write(data)
+ Expect(err).ToNot(HaveOccurred())
+ }))
+ defer svr.Close()
+
+ parsedURL, err := utils.ParseURL(svr.URL)
+ Expect(err).ToNot(HaveOccurred())
+
+ id := cpi.NewConsumerIdentity(identity.CONSUMER_TYPE)
+ id.SetNonEmptyValue(identity.ID_HOSTNAME, parsedURL.Host)
+ id.SetNonEmptyValue(identity.ID_SCHEME, parsedURL.Scheme)
+ id.SetNonEmptyValue(identity.ID_PATHPREFIX, strings.Trim(parsedURL.Path, "/"))
+ id.SetNonEmptyValue(identity.ID_PORT, parsedURL.Port())
+
+ creds := credentials.DirectCredentials{
+ cpi.ATTR_KEY: encryptionKey,
+ }
+ defaultContext.SetCredentialsForConsumer(id, creds)
+
+ spec := fmt.Sprintf(repoSpecTemplate, svr.URL, local.AESECB)
+
+ repo, err := defaultContext.RepositoryForConfig([]byte(spec), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(repo).ToNot(BeNil())
+
+ credentialsFromRepo, err := repo.LookupCredentials("test-credentials")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentialsFromRepo).To(Equal(expectedCreds))
+ })
+
+ It("retrieves credentials from file", func() {
+ filename := "/container_registry"
+ fs := memoryfs.New()
+ vfsattr.Set(defaultContext, fs)
+
+ file, err := fs.Create(filename)
+ Expect(err).ToNot(HaveOccurred())
+
+ _, err = file.Write([]byte(containerRegistryCfg))
+ Expect(err).ToNot(HaveOccurred())
+
+ err = file.Close()
+ Expect(err).ToNot(HaveOccurred())
+
+ spec := fmt.Sprintf(repoSpecTemplate, "file://"+filename, local.Plaintext)
+
+ repo, err := defaultContext.RepositoryForConfig([]byte(spec), nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(repo).ToNot(BeNil())
+
+ credentialsFromRepo, err := repo.LookupCredentials("test-credentials")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentialsFromRepo).To(Equal(expectedCreds))
+ })
+})
diff --git a/api/credentials/extensions/repositories/gardenerconfig/repository.go b/api/credentials/extensions/repositories/gardenerconfig/repository.go
new file mode 100644
index 000000000..0ef1e8fa5
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/repository.go
@@ -0,0 +1,223 @@
+package gardenerconfig
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/identity"
+ "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/utils/errkind"
+)
+
+type Cipher string
+
+const (
+ Plaintext Cipher = "PLAINTEXT"
+ AESECB Cipher = "AES.ECB"
+)
+
+type Repository struct {
+ ctx cpi.Context
+ lock sync.RWMutex
+ url string
+ configType gardenercfgcpi.ConfigType
+ cipher Cipher
+ key []byte
+ propagateConsumerIdentity bool
+ creds map[string]cpi.Credentials
+ fs vfs.FileSystem
+}
+
+var _ cpi.ConsumerIdentityProvider = (*Repository)(nil)
+
+func NewRepository(ctx cpi.Context, url string, configType gardenercfgcpi.ConfigType, cipher Cipher, key []byte, propagateConsumerIdentity bool) (*Repository, error) {
+ r := &Repository{
+ ctx: ctx,
+ url: url,
+ configType: configType,
+ cipher: cipher,
+ key: key,
+ propagateConsumerIdentity: propagateConsumerIdentity,
+ fs: vfsattr.Get(ctx),
+ }
+ if err := r.read(true); err != nil {
+ return nil, fmt.Errorf("unable to read repository content: %w", err)
+ }
+ return r, nil
+}
+
+var _ cpi.Repository = &Repository{}
+
+func (r *Repository) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
+ id, err := identity.GetConsumerId(r.url)
+ if err != nil {
+ return nil
+ }
+ return id
+}
+
+func (r *Repository) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if err := r.read(false); err != nil {
+ return false, fmt.Errorf("unable to read repository content: %w", err)
+ }
+
+ return r.creds[name] != nil, nil
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ if err := r.read(false); err != nil {
+ return nil, fmt.Errorf("unable to read repository content: %w", err)
+ }
+
+ auth, ok := r.creds[name]
+ if !ok {
+ return nil, cpi.ErrUnknownCredentials(name)
+ }
+
+ return auth, nil
+}
+
+func (r *Repository) WriteCredentials(name string, creds cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported("write", "credentials", Type)
+}
+
+func (r *Repository) read(force bool) error {
+ if !force && r.creds != nil {
+ return nil
+ }
+
+ configReader, err := r.getRawConfig()
+ if err != nil {
+ return fmt.Errorf("unable to get config: %w", err)
+ }
+ if configReader == nil {
+ return nil
+ }
+ defer configReader.Close()
+
+ handler := gardenercfgcpi.GetHandler(r.configType)
+ if handler == nil {
+ return errors.Newf("unable to get handler for config type %s", string(r.configType))
+ }
+
+ creds, err := handler.ParseConfig(configReader)
+ if err != nil {
+ return fmt.Errorf("unable to parse config: %w", err)
+ }
+
+ r.creds = map[string]cpi.Credentials{}
+ for _, cred := range creds {
+ credName := cred.Name()
+ if _, ok := r.creds[credName]; !ok {
+ r.creds[credName] = cred.Properties()
+ }
+ if r.propagateConsumerIdentity {
+ getCredentials := func() (cpi.Credentials, error) {
+ return r.LookupCredentials(credName)
+ }
+ cg := credentialGetter{
+ getCredentials: getCredentials,
+ }
+ r.ctx.SetCredentialsForConsumer(cred.ConsumerIdentity(), cg)
+ }
+ }
+
+ return nil
+}
+
+func (r *Repository) getRawConfig() (io.ReadCloser, error) {
+ u, err := url.Parse(r.url)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse url: %w", err)
+ }
+
+ var reader io.ReadCloser
+ if u.Scheme == "file" {
+ f, err := r.fs.Open(u.Path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to open file: %w", err)
+ }
+ reader = f
+ } else {
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, fmt.Errorf("request to secret server failed: %w", err)
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ // the secret server might be temporarily not available.
+ // for these situations we should allow a retry at a later point in time
+ // while keeping the old data for the moment.
+ if errkind.IsRetryable(err) {
+ // TODO: log error
+ return nil, nil
+ }
+ return nil, fmt.Errorf("request to secret server failed: %w", err)
+ }
+ reader = res.Body
+ }
+
+ switch r.cipher {
+ case AESECB:
+ var srcBuf bytes.Buffer
+ if _, err := io.Copy(&srcBuf, reader); err != nil {
+ return nil, fmt.Errorf("unable to read: %w", err)
+ }
+ if err := reader.Close(); err != nil {
+ return nil, fmt.Errorf("unable to close reader: %w", err)
+ }
+ block, err := aes.NewCipher(r.key)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create cipher: %w", err)
+ }
+ dst := make([]byte, srcBuf.Len())
+ if err := ecbDecrypt(block, dst, srcBuf.Bytes()); err != nil {
+ return nil, fmt.Errorf("unable to decrypt: %w", err)
+ }
+
+ return io.NopCloser(bytes.NewBuffer(dst)), nil
+ case Plaintext:
+ return reader, nil
+ default:
+ return nil, errors.ErrNotImplemented("cipher algorithm", string(r.cipher), Type)
+ }
+}
+
+func ecbDecrypt(block cipher.Block, dst, src []byte) error {
+ blockSize := block.BlockSize()
+ if len(src)%blockSize != 0 {
+ return fmt.Errorf("input must contain only full blocks (blocksize: %d; input length: %d)", blockSize, len(src))
+ }
+ if len(dst) < len(src) {
+ return errors.New("destination is smaller than source")
+ }
+ for len(src) > 0 {
+ block.Decrypt(dst, src[:blockSize])
+ src = src[blockSize:]
+ dst = dst[blockSize:]
+ }
+ return nil
+}
diff --git a/pkg/contexts/credentials/repositories/gardenerconfig/suite_test.go b/api/credentials/extensions/repositories/gardenerconfig/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/gardenerconfig/suite_test.go
rename to api/credentials/extensions/repositories/gardenerconfig/suite_test.go
diff --git a/api/credentials/extensions/repositories/gardenerconfig/type.go b/api/credentials/extensions/repositories/gardenerconfig/type.go
new file mode 100644
index 000000000..2b0d95952
--- /dev/null
+++ b/api/credentials/extensions/repositories/gardenerconfig/type.go
@@ -0,0 +1,96 @@
+package gardenerconfig
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/generics"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ gardenercfgcpi "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig/identity"
+ "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "GardenerConfig"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1))
+}
+
+// RepositorySpec describes a secret server based credential repository interface.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ URL string `json:"url"`
+ ConfigType gardenercfgcpi.ConfigType `json:"configType"`
+ Cipher Cipher `json:"cipher"`
+ PropagateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
+}
+
+var _ cpi.ConsumerIdentityProvider = (*RepositorySpec)(nil)
+
+// NewRepositorySpec creates a new memory RepositorySpec.
+func NewRepositorySpec(url string, configType gardenercfgcpi.ConfigType, cipher Cipher, propagateConsumerIdentity ...bool) *RepositorySpec {
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ URL: url,
+ ConfigType: configType,
+ Cipher: cipher,
+ PropagateConsumerIdentity: generics.Pointer(utils.OptionalDefaultedBool(true, propagateConsumerIdentity...)),
+ }
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) {
+ r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories)
+ repos, ok := r.(*Repositories)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to Responsitories", r)
+ }
+
+ key, err := getKey(ctx, a.URL)
+ if err != nil {
+ return nil, fmt.Errorf("unable to get key from context: %w", err)
+ }
+
+ return repos.GetRepository(ctx, a.URL, a.ConfigType, a.Cipher, key, utils.AsBool(a.PropagateConsumerIdentity, true))
+}
+
+func (a *RepositorySpec) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
+ id, err := identity.GetConsumerId(a.URL)
+ if err != nil {
+ return nil
+ }
+ return id
+}
+
+func (a *RepositorySpec) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
+
+func getKey(cctx cpi.Context, configURL string) ([]byte, error) {
+ id, err := identity.GetConsumerId(configURL)
+ if err != nil {
+ return nil, err
+ }
+
+ creds, err := cpi.CredentialsForConsumer(cctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ var key string
+ if creds != nil {
+ key = creds.GetProperty(identity.ATTR_KEY)
+ }
+
+ return []byte(key), nil
+}
diff --git a/api/credentials/extensions/repositories/init.go b/api/credentials/extensions/repositories/init.go
new file mode 100644
index 000000000..4e32290cc
--- /dev/null
+++ b/api/credentials/extensions/repositories/init.go
@@ -0,0 +1,12 @@
+package repositories
+
+import (
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/aliases"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/directcreds"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/dockerconfig"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/gardenerconfig"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/memory/config"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/npm"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories/vault"
+)
diff --git a/api/credentials/extensions/repositories/memory/cache.go b/api/credentials/extensions/repositories/memory/cache.go
new file mode 100644
index 000000000..d58c8e8be
--- /dev/null
+++ b/api/credentials/extensions/repositories/memory/cache.go
@@ -0,0 +1,31 @@
+package memory
+
+import (
+ "sync"
+
+ "ocm.software/ocm/api/datacontext"
+)
+
+const ATTR_REPOS = "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+
+type Repositories struct {
+ lock sync.Mutex
+ repos map[string]*Repository
+}
+
+func newRepositories(datacontext.Context) interface{} {
+ return &Repositories{
+ repos: map[string]*Repository{},
+ }
+}
+
+func (r *Repositories) GetRepository(name string) *Repository {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ repo := r.repos[name]
+ if repo == nil {
+ repo = NewRepository(name)
+ r.repos[name] = repo
+ }
+ return repo
+}
diff --git a/api/credentials/extensions/repositories/memory/config/config_test.go b/api/credentials/extensions/repositories/memory/config/config_test.go
new file mode 100644
index 000000000..2c0647a26
--- /dev/null
+++ b/api/credentials/extensions/repositories/memory/config/config_test.go
@@ -0,0 +1,62 @@
+package config_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/env"
+
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+var _ = Describe("configure credentials", func() {
+ var env *Environment
+ var ctx credentials.Context
+ var cfg config.Context
+
+ BeforeEach(func() {
+ env = NewEnvironment(TestData())
+ cfg = config.New()
+ ctx = credentials.WithConfigs(cfg).New()
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("reads config with ref", func() {
+ data, err := vfs.ReadFile(env, "/testdata/creds.yaml")
+ Expect(err).To(Succeed())
+ _, err = cfg.ApplyData(data, nil, "creds.yaml")
+ Expect(err).To(Succeed())
+
+ spec := memory.NewRepositorySpec("default")
+ repo, err := ctx.RepositoryForSpec(spec)
+ Expect(err).To(Succeed())
+ mem := repo.(*memory.Repository)
+ Expect(mem.ExistsCredentials("ref")).To(BeTrue())
+ creds, err := mem.LookupCredentials("ref")
+ Expect(err).To(Succeed())
+ Expect(creds.Properties()).To(Equal(common.Properties{"username": "mandelsoft", "password": "specialsecret"}))
+ })
+
+ It("reads config with direct", func() {
+ data, err := vfs.ReadFile(env, "/testdata/creds.yaml")
+ Expect(err).To(Succeed())
+ _, err = cfg.ApplyData(data, nil, "creds.yaml")
+ Expect(err).To(Succeed())
+
+ spec := memory.NewRepositorySpec("default")
+ repo, err := ctx.RepositoryForSpec(spec)
+ Expect(err).To(Succeed())
+ mem := repo.(*memory.Repository)
+ Expect(mem.ExistsCredentials("direct")).To(BeTrue())
+ creds, err := mem.LookupCredentials("direct")
+ Expect(err).To(Succeed())
+ Expect(creds.Properties()).To(Equal(common.Properties{"username": "mandelsoft2", "password": "specialsecret2"}))
+ })
+})
diff --git a/pkg/contexts/credentials/repositories/memory/config/suite_test.go b/api/credentials/extensions/repositories/memory/config/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/memory/config/suite_test.go
rename to api/credentials/extensions/repositories/memory/config/suite_test.go
diff --git a/pkg/contexts/credentials/repositories/memory/config/testdata/creds.yaml b/api/credentials/extensions/repositories/memory/config/testdata/creds.yaml
similarity index 100%
rename from pkg/contexts/credentials/repositories/memory/config/testdata/creds.yaml
rename to api/credentials/extensions/repositories/memory/config/testdata/creds.yaml
diff --git a/api/credentials/extensions/repositories/memory/config/type.go b/api/credentials/extensions/repositories/memory/config/type.go
new file mode 100644
index 000000000..95038eaa8
--- /dev/null
+++ b/api/credentials/extensions/repositories/memory/config/type.go
@@ -0,0 +1,131 @@
+package config
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "memory.credentials" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+// Config describes a configuration for the config context.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ RepoName string `json:"repoName"`
+ Credentials []CredentialsSpec `json:"credentials,omitempty"`
+}
+
+type CredentialsSpec struct {
+ CredentialsName string `json:"credentialsName"`
+ // Reference refers to credentials store in some othe repo
+ Reference *cpi.GenericCredentialsSpec `json:"reference,omitempty"`
+ // Credentials are direct credentials (one of Reference or Credentials must be set)
+ Credentials common.Properties `json:"credentials"`
+}
+
+// New creates a new memory ConfigSpec.
+func New(repo string, credentials ...CredentialsSpec) *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType),
+ RepoName: repo,
+ Credentials: credentials,
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) AddCredentials(name string, props common.Properties) error {
+ a.Credentials = append(a.Credentials, CredentialsSpec{CredentialsName: name, Credentials: props})
+ return nil
+}
+
+func (a *Config) AddCredentialsRef(name string, refname string, spec cpi.RepositorySpec) error {
+ repo, err := cpi.ToGenericRepositorySpec(spec)
+ if err != nil {
+ return fmt.Errorf("unable to convert cpi repository spec to generic: %w", err)
+ }
+
+ ref := cpi.NewGenericCredentialsSpec(refname, repo)
+ a.Credentials = append(a.Credentials, CredentialsSpec{CredentialsName: name, Reference: ref})
+
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ list := errors.ErrListf("applying config")
+
+ t, ok := target.(cpi.Context)
+ if !ok {
+ return cfgcpi.ErrNoContext(ConfigType)
+ }
+
+ repo, err := t.RepositoryForSpec(memory.NewRepositorySpec(a.RepoName))
+ if err != nil {
+ return fmt.Errorf("unable to get repository for spec: %w", err)
+ }
+
+ mem, ok := repo.(*memory.Repository)
+ if !ok {
+ return fmt.Errorf("invalid type assertion of type %T to memory.Repository", repo)
+ }
+
+ for i, e := range a.Credentials {
+ var creds cpi.Credentials
+ if e.Reference != nil {
+ if len(e.Credentials) != 0 {
+ err = fmt.Errorf("credentials and reference set")
+ } else {
+ creds, err = e.Reference.Credentials(t)
+ }
+ } else {
+ creds = cpi.NewCredentials(e.Credentials)
+ }
+ if err != nil {
+ list.Add(errors.Wrapf(err, "config entry %d[%s]", i, e.CredentialsName))
+ }
+ if creds != nil {
+ _, err = mem.WriteCredentials(e.CredentialsName, creds)
+ if err != nil {
+ list.Add(errors.Wrapf(err, "config entry %d", i))
+ }
+ }
+ }
+ return list.Result()
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define a list
+of arbitrary credentials stored in a memory based credentials repository:
+
++ type: ` + ConfigType + ` + repoName: default + credentials: + - credentialsName: ref + reference: # refer to a credential set stored in some other credential repository + type: Credentials # this is a repo providing just one explicit credential set + properties: + username: mandelsoft + password: specialsecret + - credentialsName: direct + credentials: # direct credential specification + username: mandelsoft2 + password: specialsecret2 ++` diff --git a/api/credentials/extensions/repositories/memory/repo_test.go b/api/credentials/extensions/repositories/memory/repo_test.go new file mode 100644 index 000000000..c1a244f48 --- /dev/null +++ b/api/credentials/extensions/repositories/memory/repo_test.go @@ -0,0 +1,119 @@ +package memory_test + +import ( + "encoding/json" + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/credentials" + local "ocm.software/ocm/api/credentials/extensions/repositories/memory" + common "ocm.software/ocm/api/utils/misc" +) + +var DefaultContext = credentials.New() + +var _ = Describe("direct credentials", func() { + props := common.Properties{ + "user": "USER", + "password": "PASSWORD", + } + + props2 := common.Properties{ + "user": "OTHER", + "password": "OTHERPASSWORD", + } + + specdata := "{\"type\":\"Memory\",\"repoName\":\"test\"}" + + _ = props + + It("serializes repo spec", func() { + spec := local.NewRepositorySpec("test") + data, err := json.Marshal(spec) + Expect(err).To(Succeed()) + Expect(data).To(Equal([]byte(specdata))) + }) + It("deserializes repo spec", func() { + spec, err := DefaultContext.RepositorySpecForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + Expect(reflect.TypeOf(spec).String()).To(Equal("*memory.RepositorySpec")) + Expect(spec.(*local.RepositorySpec).RepositoryName).To(Equal("test")) + }) + + It("resolves repository", func() { + repo, err := DefaultContext.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + Expect(reflect.TypeOf(repo).String()).To(Equal("*memory.Repository")) + }) + + It("sets and retrieves credentials", func() { + repo, err := DefaultContext.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + + _, err = repo.WriteCredentials("bibo", credentials.NewCredentials(props)) + Expect(err).To(Succeed()) + + creds, err := repo.LookupCredentials("bibo") + Expect(err).To(Succeed()) + Expect(creds.Properties()).To(Equal(props)) + + creds, err = repo.LookupCredentials("other") + Expect(err).NotTo(Succeed()) + Expect(creds).To(BeNil()) + }) + + It("caches repo", func() { + repo, err := DefaultContext.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + + _, err = repo.WriteCredentials("bibo", credentials.NewCredentials(props)) + Expect(err).To(Succeed()) + + // re-request repo by spec + repo, err = DefaultContext.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + + creds, err := repo.LookupCredentials("bibo") + Expect(err).To(Succeed()) + Expect(creds.Properties()).To(Equal(props)) + + creds, err = repo.LookupCredentials("other") + Expect(err).NotTo(Succeed()) + Expect(creds).To(BeNil()) + }) + + It("caches repo in two contexts", func() { + ctx1 := DefaultContext + ctx2 := credentials.New() + + // write to first context + repo1, err := ctx1.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + + _, err = repo1.WriteCredentials("bibo", credentials.NewCredentials(props)) + Expect(err).To(Succeed()) + + // request repo in second context + repo2, err := ctx2.RepositoryForConfig([]byte(specdata), nil) + Expect(err).To(Succeed()) + + creds, err := repo2.LookupCredentials("bibo") + Expect(err).NotTo(Succeed()) + Expect(creds).To(BeNil()) + + // write to second context + _, err = repo2.WriteCredentials("bibo", credentials.NewCredentials(props2)) + Expect(err).To(Succeed()) + + creds, err = repo2.LookupCredentials("bibo") + Expect(err).To(Succeed()) + Expect(creds.Properties()).To(Equal(props2)) + + // check first context + creds, err = repo1.LookupCredentials("bibo") + Expect(err).To(Succeed()) + Expect(creds.Properties()).To(Equal(props)) + }) +}) diff --git a/api/credentials/extensions/repositories/memory/repository.go b/api/credentials/extensions/repositories/memory/repository.go new file mode 100644 index 000000000..3b93c1a99 --- /dev/null +++ b/api/credentials/extensions/repositories/memory/repository.go @@ -0,0 +1,47 @@ +package memory + +import ( + "sync" + + "ocm.software/ocm/api/credentials/cpi" +) + +type Repository struct { + lock sync.RWMutex + name string + credentials map[string]cpi.Credentials +} + +func NewRepository(name string) *Repository { + return &Repository{ + name: name, + credentials: map[string]cpi.Credentials{}, + } +} + +var _ cpi.Repository = &Repository{} + +func (r *Repository) ExistsCredentials(name string) (bool, error) { + r.lock.RLock() + defer r.lock.RUnlock() + _, ok := r.credentials[name] + return ok, nil +} + +func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) { + r.lock.RLock() + defer r.lock.RUnlock() + c, ok := r.credentials[name] + if ok { + return cpi.NewCredentials(c.Properties()), nil + } + return nil, cpi.ErrUnknownCredentials(name) +} + +func (r *Repository) WriteCredentials(name string, creds cpi.Credentials) (cpi.Credentials, error) { + c := cpi.NewCredentials(creds.Properties()) + r.lock.Lock() + defer r.lock.Unlock() + r.credentials[name] = c + return c, nil +} diff --git a/pkg/contexts/credentials/repositories/memory/suite_test.go b/api/credentials/extensions/repositories/memory/suite_test.go similarity index 100% rename from pkg/contexts/credentials/repositories/memory/suite_test.go rename to api/credentials/extensions/repositories/memory/suite_test.go diff --git a/api/credentials/extensions/repositories/memory/type.go b/api/credentials/extensions/repositories/memory/type.go new file mode 100644 index 000000000..97dc31a44 --- /dev/null +++ b/api/credentials/extensions/repositories/memory/type.go @@ -0,0 +1,45 @@ +package memory + +import ( + "fmt" + + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + Type = "Memory" + TypeV1 = Type + runtime.VersionSeparator + "v1" +) + +func init() { + cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type)) + cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1)) +} + +// RepositorySpec describes a memory based repository interface. +type RepositorySpec struct { + runtime.ObjectVersionedType `json:",inline"` + RepositoryName string `json:"repoName"` +} + +// NewRepositorySpec creates a new memory RepositorySpec. +func NewRepositorySpec(name string) *RepositorySpec { + return &RepositorySpec{ + ObjectVersionedType: runtime.NewVersionedTypedObject(Type), + RepositoryName: name, + } +} + +func (a *RepositorySpec) GetType() string { + return Type +} + +func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) { + r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories) + repos, ok := r.(*Repositories) + if !ok { + return nil, fmt.Errorf("failed to assert type %T to Repositories", r) + } + return repos.GetRepository(a.RepositoryName), nil +} diff --git a/api/credentials/extensions/repositories/npm/a_usage.go b/api/credentials/extensions/repositories/npm/a_usage.go new file mode 100644 index 000000000..888a6ac21 --- /dev/null +++ b/api/credentials/extensions/repositories/npm/a_usage.go @@ -0,0 +1,18 @@ +package npm + +import ( + "ocm.software/ocm/api/utils/listformat" +) + +var usage = ` +This repository type can be used to access credentials stored in a file +following the NPM npmrc format (~/.npmrc). It take into account the +credentials helper section, also. If enabled, the described +credentials will be automatically assigned to appropriate consumer ids. +` + +var format = `The repository specification supports the following fields: +` + listformat.FormatListElements("", listformat.StringElementDescriptionList{ + "npmrcFile", "*string*: the file path to a NPM npmrc file", + "propagateConsumerIdentity", "*bool*(optional): enable consumer id propagation", +}) diff --git a/api/credentials/extensions/repositories/npm/cache.go b/api/credentials/extensions/repositories/npm/cache.go new file mode 100644 index 000000000..fe3d8fd39 --- /dev/null +++ b/api/credentials/extensions/repositories/npm/cache.go @@ -0,0 +1,33 @@ +package npm + +import ( + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/datacontext" +) + +type Cache struct { + repos map[string]*Repository +} + +func createCache(_ datacontext.Context) interface{} { + return &Cache{ + repos: map[string]*Repository{}, + } +} + +func (r *Cache) GetRepository(ctx cpi.Context, name string, prop bool) (*Repository, error) { + var ( + err error = nil + repo *Repository + ) + if name != "" { + repo = r.repos[name] + } + if repo == nil { + repo, err = NewRepository(ctx, name, prop) + if err == nil { + r.repos[name] = repo + } + } + return repo, err +} diff --git a/api/credentials/extensions/repositories/npm/config.go b/api/credentials/extensions/repositories/npm/config.go new file mode 100644 index 000000000..eb22e51f8 --- /dev/null +++ b/api/credentials/extensions/repositories/npm/config.go @@ -0,0 +1,56 @@ +package npm + +import ( + "bufio" + "os" + "strings" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/utils" +) + +type npmConfig map[string]string + +// readNpmConfigFile reads "~/.npmrc" file line by line, parse it and return the result as a npmConfig. +func readNpmConfigFile(path string) (npmConfig, string, error) { + path, err := utils.ResolvePath(path) + if err != nil { + return nil, path, errors.Wrapf(err, "cannot resolve path %q", path) + } + + // Open the file + file, err := os.Open(path) + if err != nil { + return nil, path, err + } + defer file.Close() + + // Create a new scanner and read the file line by line + scanner := bufio.NewScanner(file) + cfg := make(map[string]string) + for scanner.Scan() { + line := scanner.Text() + line, authFound := strings.CutPrefix(line, "//") + if !authFound { + // e.g. 'global=false' + continue + } + // Split the line into key and value + parts := strings.SplitN(line, ":_authToken=", 2) + if len(parts) == 2 { + if strings.HasSuffix(parts[0], "/") { + cfg[parts[0][:len(parts[0])-1]] = parts[1] + } else { + cfg[parts[0]] = parts[1] + } + } + } + + // Check for errors + if err = scanner.Err(); err != nil { + return nil, path, err + } + + return cfg, path, nil +} diff --git a/api/credentials/extensions/repositories/npm/config_test.go b/api/credentials/extensions/repositories/npm/config_test.go new file mode 100644 index 000000000..b6dacfef3 --- /dev/null +++ b/api/credentials/extensions/repositories/npm/config_test.go @@ -0,0 +1,38 @@ +package npm_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/builtin/npm/identity" + "ocm.software/ocm/api/credentials/extensions/repositories/npm" + common "ocm.software/ocm/api/utils/misc" +) + +var _ = Describe("Config deserialization Test Environment", func() { + It("read .npmrc", func() { + ctx := credentials.New() + repo := Must(npm.NewRepository(ctx, "testdata/.npmrc")) + Expect(Must(repo.LookupCredentials("registry.npmjs.org")).Properties()).To(Equal(common.Properties{identity.ATTR_TOKEN: "npm_TOKEN"})) + Expect(Must(repo.LookupCredentials("npm.registry.acme.com/api/npm")).Properties()).To(Equal(common.Properties{identity.ATTR_TOKEN: "bearer_TOKEN"})) + }) + + It("propagates credentials", func() { + ctx := credentials.New() + spec := npm.NewRepositorySpec("testdata/.npmrc") + _ = Must(ctx.RepositoryForSpec(spec)) + id := Must(identity.GetConsumerId("registry.npmjs.org", "pkg")) + creds := Must(credentials.CredentialsForConsumer(ctx, id)) + Expect(creds).NotTo(BeNil()) + Expect(creds.GetProperty(identity.ATTR_TOKEN)).To(Equal("npm_TOKEN")) + }) + + It("has description", func() { + ctx := credentials.New() + t := ctx.RepositoryTypes().GetType(npm.TypeV1) + Expect(t).NotTo(BeNil()) + Expect(t.Description()).NotTo(Equal("")) + }) +}) diff --git a/api/credentials/extensions/repositories/npm/default.go b/api/credentials/extensions/repositories/npm/default.go new file mode 100644 index 000000000..261b6e900 --- /dev/null +++ b/api/credentials/extensions/repositories/npm/default.go @@ -0,0 +1,49 @@ +package npm + +import ( + "fmt" + "os" + + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/config" + credcfg "ocm.software/ocm/api/credentials/config" + "ocm.software/ocm/api/ocm/ocmutils/defaultconfigregistry" +) + +const ( + ConfigFileName = ".npmrc" +) + +func init() { + defaultconfigregistry.RegisterDefaultConfigHandler(DefaultConfigHandler, desc) +} + +func DefaultConfig() (string, error) { + d, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(d, ConfigFileName), nil +} + +func DefaultConfigHandler(cfg config.Context) (string, config.Config, error) { + // use docker config as default config for ocm cli + d, err := DefaultConfig() + if err != nil { + return "", nil, nil + } + if ok, err := vfs.FileExists(osfs.OsFs, d); ok && err == nil { + ccfg := credcfg.New() + ccfg.AddRepository(NewRepositorySpec(d, true)) + return d, ccfg, nil + } + return "", nil, nil +} + +var desc = fmt.Sprintf(` +The npm configuration file at
~/%s
is
+read to feed in the configured credentials for NPM registries.
+`, ConfigFileName)
diff --git a/api/credentials/extensions/repositories/npm/provider.go b/api/credentials/extensions/repositories/npm/provider.go
new file mode 100644
index 000000000..dd3cc4630
--- /dev/null
+++ b/api/credentials/extensions/repositories/npm/provider.go
@@ -0,0 +1,51 @@
+package npm
+
+import (
+ npm "ocm.software/ocm/api/credentials/builtin/npm/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils/logging"
+)
+
+type ConsumerProvider struct {
+ npmrcPath string
+}
+
+var _ cpi.ConsumerProvider = (*ConsumerProvider)(nil)
+
+func (p *ConsumerProvider) Unregister(_ cpi.ProviderIdentity) {
+}
+
+func (p *ConsumerProvider) Match(ectx cpi.EvaluationContext, req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ return p.get(req, cur, m)
+}
+
+func (p *ConsumerProvider) Get(req cpi.ConsumerIdentity) (cpi.CredentialsSource, bool) {
+ creds, _ := p.get(req, nil, cpi.CompleteMatch)
+ return creds, creds != nil
+}
+
+func (p *ConsumerProvider) get(requested cpi.ConsumerIdentity, currentFound cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ all, path, err := readNpmConfigFile(p.npmrcPath)
+ if err != nil {
+ log := logging.Context().Logger(npm.REALM)
+ log.LogError(err, "Failed to read npmrc file", "path", path)
+ return nil, nil
+ }
+
+ var creds cpi.CredentialsSource
+
+ for key, value := range all {
+ id, err := npm.GetConsumerId("https://"+key, "")
+ if err != nil {
+ log := logging.Context().Logger(npm.REALM)
+ log.LogError(err, "Failed to get consumer id", "key", key, "value", value)
+ return nil, nil
+ }
+ if m(requested, currentFound, id) {
+ creds = newCredentials(value)
+ currentFound = id
+ }
+ }
+
+ return creds, currentFound
+}
diff --git a/api/credentials/extensions/repositories/npm/repository.go b/api/credentials/extensions/repositories/npm/repository.go
new file mode 100644
index 000000000..6107f1b1a
--- /dev/null
+++ b/api/credentials/extensions/repositories/npm/repository.go
@@ -0,0 +1,88 @@
+package npm
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ npmCredentials "ocm.software/ocm/api/credentials/builtin/npm/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const PROVIDER = "ocm.software/credentialprovider/" + Type
+
+type Repository struct {
+ ctx cpi.Context
+ path string
+ propagate bool
+ npmrc npmConfig
+}
+
+func NewRepository(ctx cpi.Context, path string, prop ...bool) (*Repository, error) {
+ return newRepository(ctx, path, utils.OptionalDefaultedBool(true, prop...))
+}
+
+func newRepository(ctx cpi.Context, path string, prop bool) (*Repository, error) {
+ r := &Repository{
+ ctx: ctx,
+ path: path,
+ propagate: prop,
+ }
+ err := r.Read(true)
+ return r, err
+}
+
+var _ cpi.Repository = &Repository{}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ err := r.Read(false)
+ if err != nil {
+ return false, err
+ }
+ return r.npmrc[name] != "", nil
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ exists, err := r.ExistsCredentials(name)
+ if err != nil {
+ return nil, err
+ }
+ if !exists {
+ return nil, errors.ErrNotFound("credentials", name, Type)
+ }
+ return newCredentials(r.npmrc[name]), nil
+}
+
+func (r *Repository) WriteCredentials(_ string, _ cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported("write", "credentials", Type)
+}
+
+func (r *Repository) Read(force bool) error {
+ if !force && r.npmrc != nil {
+ return nil
+ }
+
+ if r.path == "" {
+ return errors.New("npmrc path not provided")
+ }
+ cfg, path, err := readNpmConfigFile(r.path)
+ if err != nil {
+ return fmt.Errorf("failed to load npmrc: %w", err)
+ }
+ id := cpi.ProviderIdentity(PROVIDER + "/" + path)
+
+ if r.propagate {
+ r.ctx.RegisterConsumerProvider(id, &ConsumerProvider{r.path})
+ }
+ r.npmrc = cfg
+ return nil
+}
+
+func newCredentials(token string) cpi.Credentials {
+ props := common.Properties{
+ npmCredentials.ATTR_TOKEN: token,
+ }
+ return cpi.NewCredentials(props)
+}
diff --git a/api/credentials/extensions/repositories/npm/repository_test.go b/api/credentials/extensions/repositories/npm/repository_test.go
new file mode 100644
index 000000000..6e6daf942
--- /dev/null
+++ b/api/credentials/extensions/repositories/npm/repository_test.go
@@ -0,0 +1,77 @@
+package npm_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ npmCredentials "ocm.software/ocm/api/credentials/builtin/npm/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ local "ocm.software/ocm/api/credentials/extensions/repositories/npm"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+var _ = Describe("NPM config - .npmrc", func() {
+ props := common.Properties{
+ npmCredentials.ATTR_TOKEN: "npm_TOKEN",
+ }
+
+ props2 := common.Properties{
+ npmCredentials.ATTR_TOKEN: "bearer_TOKEN",
+ }
+
+ var DefaultContext credentials.Context
+
+ BeforeEach(func() {
+ DefaultContext = credentials.New()
+ })
+
+ specdata := "{\"type\":\"NPMConfig\",\"npmrcFile\":\"testdata/.npmrc\"}"
+
+ It("serializes repo spec", func() {
+ spec := local.NewRepositorySpec("testdata/.npmrc")
+ data := Must(json.Marshal(spec))
+ Expect(data).To(Equal([]byte(specdata)))
+ })
+
+ It("deserializes repo spec", func() {
+ spec := Must(DefaultContext.RepositorySpecForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(spec).String()).To(Equal("*npm.RepositorySpec"))
+ Expect(spec.(*local.RepositorySpec).NpmrcFile).To(Equal("testdata/.npmrc"))
+ })
+
+ It("resolves repository", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(repo).String()).To(Equal("*npm.Repository"))
+ })
+
+ It("retrieves credentials", func() {
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+
+ creds := Must(repo.LookupCredentials("registry.npmjs.org"))
+ Expect(creds.Properties()).To(Equal(props))
+
+ creds = Must(repo.LookupCredentials("npm.registry.acme.com/api/npm"))
+ Expect(creds.Properties()).To(Equal(props2))
+ })
+
+ It("can access the default context", func() {
+ ctx := credentials.New()
+
+ r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx)
+ Expect(r).NotTo(BeNil())
+
+ Must(ctx.RepositoryForConfig([]byte(specdata), nil))
+
+ ci := cpi.NewConsumerIdentity(npmCredentials.CONSUMER_TYPE)
+ Expect(ci).NotTo(BeNil())
+ credentials := Must(cpi.CredentialsForConsumer(ctx.CredentialsContext(), ci))
+ Expect(credentials).NotTo(BeNil())
+ Expect(credentials.Properties()).To(Equal(props))
+ })
+})
diff --git a/pkg/contexts/credentials/repositories/npm/suite_test.go b/api/credentials/extensions/repositories/npm/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/npm/suite_test.go
rename to api/credentials/extensions/repositories/npm/suite_test.go
diff --git a/pkg/contexts/credentials/repositories/npm/testdata/.npmrc b/api/credentials/extensions/repositories/npm/testdata/.npmrc
similarity index 100%
rename from pkg/contexts/credentials/repositories/npm/testdata/.npmrc
rename to api/credentials/extensions/repositories/npm/testdata/.npmrc
diff --git a/api/credentials/extensions/repositories/npm/type.go b/api/credentials/extensions/repositories/npm/type.go
new file mode 100644
index 000000000..91fca87d2
--- /dev/null
+++ b/api/credentials/extensions/repositories/npm/type.go
@@ -0,0 +1,62 @@
+package npm
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/generics"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ // Type is the type of the NPMConfig.
+ Type = "NPMConfig"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1, cpi.WithDescription(usage), cpi.WithFormatSpec(format)))
+}
+
+// RepositorySpec describes a docker npmrc based credential repository interface.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ NpmrcFile string `json:"npmrcFile,omitempty"`
+ PropgateConsumerIdentity *bool `json:"propagateConsumerIdentity,omitempty"`
+}
+
+// NewRepositorySpec creates a new memory RepositorySpec.
+func NewRepositorySpec(path string, propagate ...bool) *RepositorySpec {
+ var p *bool
+ if path == "" {
+ d, err := DefaultConfig()
+ if err == nil {
+ path = d
+ }
+ }
+ if len(propagate) > 0 {
+ p = generics.Pointer(utils.OptionalDefaultedBool(true, propagate...))
+ }
+
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ NpmrcFile: path,
+ PropgateConsumerIdentity: p,
+ }
+}
+
+func (rs *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (rs *RepositorySpec) Repository(ctx cpi.Context, _ cpi.Credentials) (cpi.Repository, error) {
+ r := ctx.GetAttributes().GetOrCreateAttribute(".npmrc", createCache)
+ cache, ok := r.(*Cache)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to Cache", r)
+ }
+ return cache.GetRepository(ctx, rs.NpmrcFile, utils.AsBool(rs.PropgateConsumerIdentity, true))
+}
diff --git a/api/credentials/extensions/repositories/vault/a_usage.go b/api/credentials/extensions/repositories/vault/a_usage.go
new file mode 100644
index 000000000..592fca2d3
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/a_usage.go
@@ -0,0 +1,56 @@
+package vault
+
+import (
+ "strings"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+func init() {
+ info := cpi.DefaultContext.ConsumerIdentityMatchers().GetInfo(identity.CONSUMER_TYPE)
+ idx := strings.Index(info.Description, "\n")
+ desc := `
+This repository type can be used to access credentials stored in a HashiCorp
+Vault.
+
+It provides access to list of secrets stored under a dedicated path in
+a vault namespace. This list can either explicitly be specified, or
+it is taken from the metadata of a specified secret.
+
+The following custom metadata attributes are evaluated:
+- ` + CUSTOM_SECRETS + `
this attribute may contain a comma separated list of
+ vault secrets, which should be exposed by this repository instance.
+ The names are evaluated under the path prefix used for the repository.
+- ` + CUSTOM_CONSUMERID + `
this attribute may contain a JSON encoded
+ consumer id , this secret should be assigned to.
+- type
if no special attribute is defined this attribute
+ indicated to use the complete custom metadata as consumer id.
+
+It uses the ` + identity.CONSUMER_TYPE + ` identity matcher and consumer type
+to requests credentials for the access.
+` + info.Description[idx:] + `
+
+It requires the following credential attributes:
+
+` + info.CredentialAttributes
+
+ usage = desc
+}
+
+var usage string
+
+var format = `
+The repository specification supports the following fields:
+` + listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ "serverURL", "*string* (required): the URL of the vault instance",
+ "namespace", "*string* (optional): the namespace used to evaluate secrets",
+ "mountPath", "*string* (optional): the mount path to use (default: secrets)",
+ "path", "*string* (optional): the path prefix used to lookup secrets",
+ "secrets", "*[]string* (optional): list of secrets",
+ "propagateConsumerIdentity", "*bool*(optional): evaluate metadata for consumer id propagation",
+}) + `
+If the secrets list is empty, all secret entries found in the given path
+is read.
+`
diff --git a/pkg/contexts/credentials/repositories/vault/auth.go b/api/credentials/extensions/repositories/vault/auth.go
similarity index 94%
rename from pkg/contexts/credentials/repositories/vault/auth.go
rename to api/credentials/extensions/repositories/vault/auth.go
index 6df7355dc..88cd6e988 100644
--- a/pkg/contexts/credentials/repositories/vault/auth.go
+++ b/api/credentials/extensions/repositories/vault/auth.go
@@ -10,8 +10,8 @@ import (
"github.com/mandelsoft/goutils/errors"
"golang.org/x/exp/maps"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/vault/identity"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
)
type AuthMethod interface {
diff --git a/api/credentials/extensions/repositories/vault/cache.go b/api/credentials/extensions/repositories/vault/cache.go
new file mode 100644
index 000000000..e45fb1a8f
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/cache.go
@@ -0,0 +1,44 @@
+package vault
+
+import (
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/datacontext"
+)
+
+const ATTR_REPOS = "ocm.software/ocm/api/credentials/extensions/repositories/vault"
+
+type Repositories struct {
+ lock sync.Mutex
+ repos map[cpi.ProviderIdentity]*Repository
+}
+
+func newRepositories(datacontext.Context) interface{} {
+ return &Repositories{
+ repos: map[cpi.ProviderIdentity]*Repository{},
+ }
+}
+
+func (r *Repositories) GetRepository(ctx cpi.Context, spec *RepositorySpec) (*Repository, error) {
+ var repo *Repository
+
+ if spec.ServerURL == "" {
+ return nil, errors.ErrInvalid("server url")
+ }
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ var err error
+ key := spec.GetKey()
+ repo = r.repos[key]
+ if repo == nil {
+ repo, err = NewRepository(ctx, spec)
+ if err == nil {
+ r.repos[key] = repo
+ }
+ }
+ return repo, err
+}
diff --git a/api/credentials/extensions/repositories/vault/identity/identity.go b/api/credentials/extensions/repositories/vault/identity/identity.go
new file mode 100644
index 000000000..af8f66907
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/identity/identity.go
@@ -0,0 +1,124 @@
+package identity
+
+import (
+ "net"
+ "net/url"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+const CONSUMER_TYPE = "HashiCorpVault"
+
+// identity properties.
+const (
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_SCHEMA = hostpath.ID_SCHEME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+ ID_MOUNTPATH = "mountPath"
+ ID_NAMESPACE = "namespace"
+)
+
+// credential properties.
+const (
+ ATTR_AUTHMETH = "authmeth"
+ ATTR_TOKEN = cpi.ATTR_TOKEN
+ ATTR_ROLEID = "roleid"
+ ATTR_SECRETID = "secretid"
+)
+
+const (
+ AUTH_APPROLE = "approle"
+ AUTH_TOKEN = "token"
+)
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(request, cur, id cpi.ConsumerIdentity) bool {
+ if id[ID_NAMESPACE] != request[ID_NAMESPACE] {
+ return false
+ }
+ if id[ID_MOUNTPATH] != "" && id[ID_MOUNTPATH] != request[ID_MOUNTPATH] {
+ return false
+ }
+ return identityMatcher(request, cur, id)
+}
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_AUTHMETH, "auth method",
+ ATTR_TOKEN, "vault token",
+ ATTR_ROLEID, "applrole role id",
+ ATTR_SECRETID, "applrole secret id",
+ })
+ ids := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ID_HOSTNAME, "vault server host",
+ ID_SCHEMA, "(optional) URL scheme",
+ ID_PORT, "(optional) server port",
+ ID_NAMESPACE, "vault namespace",
+ ID_MOUNTPATH, "mount path",
+ ID_PATHPREFIX, "path prefix for secret",
+ })
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, identityMatcher,
+ `HashiCorp Vault credential matcher
+
+This matcher matches credentials for a HashiCorp vault instance.
+It uses the following identity attributes:
+`+ids,
+ attrs+`
+The only supported auth methods, so far, are token
and approle
.
+`)
+}
+
+func GetConsumerId(serverurl string, namespace string, mountpath string, secretpath string) (cpi.ConsumerIdentity, error) {
+ if serverurl == "" {
+ return nil, errors.Newf("server address must be given")
+ }
+ u, err := url.Parse(serverurl)
+ if err != nil {
+ return nil, errors.ErrInvalidWrap(err, "server url", serverurl)
+ }
+
+ host, port, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ if strings.LastIndex(host, ":") >= 0 {
+ return nil, errors.ErrInvalidWrap(err, "server url", serverurl)
+ }
+ host = u.Host
+ }
+
+ id := cpi.ConsumerIdentity{
+ cpi.ID_TYPE: CONSUMER_TYPE,
+ ID_HOSTNAME: host,
+ }
+ if u.Scheme != "" {
+ id[ID_SCHEMA] = u.Scheme
+ }
+ if port != "" {
+ id[ID_PORT] = port
+ }
+ if namespace != "" {
+ id[ID_NAMESPACE] = namespace
+ }
+ if mountpath != "" {
+ id[ID_MOUNTPATH] = mountpath
+ }
+
+ if secretpath != "" {
+ id[ID_PATHPREFIX] = secretpath
+ }
+ return id, nil
+}
+
+func GetCredentials(ctx cpi.ContextProvider, serverurl, namespace string, mountpath, secretpath string) (cpi.Credentials, error) {
+ id, err := GetConsumerId(serverurl, namespace, mountpath, secretpath)
+ if err != nil {
+ return nil, err
+ }
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id, IdentityMatcher)
+}
diff --git a/api/credentials/extensions/repositories/vault/logging.go b/api/credentials/extensions/repositories/vault/logging.go
new file mode 100644
index 000000000..fcb526166
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/logging.go
@@ -0,0 +1,10 @@
+package vault
+
+import (
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var (
+ REALM = ocmlog.DefineSubRealm("HashiCorp Vault Access", "credentials", "vault")
+ log = ocmlog.DynamicLogger(REALM)
+)
diff --git a/api/credentials/extensions/repositories/vault/options.go b/api/credentials/extensions/repositories/vault/options.go
new file mode 100644
index 000000000..8059ffade
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/options.go
@@ -0,0 +1,96 @@
+package vault
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/utils"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ Namespace string `json:"namespace,omitempty"`
+ MountPath string `json:"mountPath,omitempty"`
+ Path string `json:"path,omitempty"`
+ Secrets []string `json:"secrets,omitempty"`
+ PropgateConsumerIdentity bool `json:"propagateConsumerIdentity,omitempty"`
+}
+
+var _ Option = (*Options)(nil)
+
+func (o *Options) ApplyTo(opts *Options) {
+ if o.Namespace != "" {
+ opts.Namespace = o.Namespace
+ }
+ if o.MountPath != "" {
+ opts.MountPath = o.MountPath
+ }
+ if o.Path != "" {
+ opts.Path = o.Path
+ }
+ if o.Secrets != nil {
+ opts.Secrets = slices.Clone(o.Secrets)
+ }
+ opts.PropgateConsumerIdentity = o.PropgateConsumerIdentity
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type ns string
+
+func (o ns) ApplyTo(opts *Options) {
+ opts.Namespace = string(o)
+}
+
+func WithNamespace(s string) Option {
+ return ns(s)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type m string
+
+func (o m) ApplyTo(opts *Options) {
+ opts.MountPath = string(o)
+}
+
+func WithMountPath(s string) Option {
+ return m(s)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type p string
+
+func (o p) ApplyTo(opts *Options) {
+ opts.Path = string(o)
+}
+
+func WithPath(s string) Option {
+ return p(s)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type sec []string
+
+func (o sec) ApplyTo(opts *Options) {
+ opts.Secrets = append(opts.Secrets, []string(o)...)
+}
+
+func WithSecrets(s ...string) Option {
+ return sec(slices.Clone(s))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type pr bool
+
+func (o pr) ApplyTo(opts *Options) {
+ opts.PropgateConsumerIdentity = bool(o)
+}
+
+func WithPropagation(b ...bool) Option {
+ return pr(utils.OptionalDefaultedBool(true, b...))
+}
diff --git a/api/credentials/extensions/repositories/vault/provider.go b/api/credentials/extensions/repositories/vault/provider.go
new file mode 100644
index 000000000..414eb8d23
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/provider.go
@@ -0,0 +1,320 @@
+package vault
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "path"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hashicorp/vault-client-go"
+ "github.com/mandelsoft/goutils/errors"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ "ocm.software/ocm/api/credentials/internal"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const PROVIDER = "ocm.software/credentialprovider/" + Type
+
+const (
+ CUSTOM_SECRETS = "secrets"
+ CUSTOM_CONSUMERID = "consumerId"
+)
+
+type mapping struct {
+ Id cpi.ConsumerIdentity
+ Name string
+}
+
+type credentialCache struct {
+ creds cpi.CredentialsSource
+ credentials map[string]cpi.DirectCredentials
+ consumer []*mapping
+}
+
+func newCredentialCache(creds cpi.CredentialsSource) *credentialCache {
+ return &credentialCache{
+ creds: creds,
+ credentials: map[string]cpi.DirectCredentials{},
+ }
+}
+
+type ConsumerProvider struct {
+ lock sync.Mutex
+ repository *Repository
+ cache *credentialCache
+
+ updated bool
+}
+
+var (
+ _ cpi.ConsumerProvider = (*ConsumerProvider)(nil)
+ _ cpi.ConsumerIdentityProvider = (*ConsumerProvider)(nil)
+)
+
+func NewConsumerProvider(repo *Repository) (*ConsumerProvider, error) {
+ src, err := repo.ctx.GetCredentialsForConsumer(repo.id)
+ if err != nil {
+ return nil, err
+ }
+ return &ConsumerProvider{
+ cache: newCredentialCache(src),
+ repository: repo,
+ }, nil
+}
+
+func (p *ConsumerProvider) String() string {
+ return p.repository.id.String()
+}
+
+func (p *ConsumerProvider) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
+ return p.repository.GetConsumerId()
+}
+
+func (p *ConsumerProvider) GetIdentityMatcher() string {
+ return p.repository.GetIdentityMatcher()
+}
+
+func (p *ConsumerProvider) update(ectx cpi.EvaluationContext) error {
+ if p.updated {
+ return nil
+ }
+ credsrc, err := cpi.GetCredentialsForConsumer(p.repository.ctx, ectx, p.repository.id, identity.IdentityMatcher)
+ if err != nil {
+ return err
+ }
+ creds, err := credsrc.Credentials(p.repository.ctx)
+ if err != nil {
+ return err
+ }
+ err = p.validateCreds(creds)
+ if err != nil {
+ return err
+ }
+
+ ctx := context.Background()
+
+ client, err := vault.New(
+ vault.WithAddress(p.repository.spec.ServerURL),
+ vault.WithRequestTimeout(30*time.Second),
+ )
+ if err != nil {
+ return err
+ }
+
+ // vault.WithMountPath("piper/PIPELINE-GROUP-4953/PIPELINE-25042/appRoleCredentials"),
+ token, err := p.getToken(ctx, client, creds)
+ if err != nil {
+ return err
+ }
+
+ if err := client.SetToken(token); err != nil {
+ return err
+ }
+ if err := client.SetNamespace(p.repository.spec.Namespace); err != nil {
+ return err
+ }
+
+ cache := newCredentialCache(credsrc)
+
+ // TODO: support for pure path based access for other secret engine types
+ secrets := slices.Clone(p.repository.spec.Secrets)
+ if len(secrets) == 0 {
+ s, err := client.Secrets.KvV2List(ctx, p.repository.spec.Path,
+ vault.WithMountPath(p.repository.spec.MountPath))
+ if err != nil {
+ p.error(err, "error listing secrets", "")
+ return err
+ }
+ for _, k := range s.Data.Keys {
+ if !strings.HasSuffix(k, "/") {
+ secrets = append(secrets, k)
+ }
+ }
+ }
+ for i := 0; i < len(secrets); i++ {
+ n := secrets[i]
+ creds, id, list, err := p.read(ctx, client, n)
+ p.error(err, "error reading vault secret", n)
+ if err == nil {
+ for _, a := range list {
+ if !slices.Contains(secrets, a) {
+ secrets = append(secrets, a)
+ }
+ }
+ if len(id) > 0 {
+ cache.consumer = append(cache.consumer, &mapping{
+ Id: cpi.ConsumerIdentity(id),
+ Name: n,
+ })
+ }
+ if len(creds) > 0 {
+ cache.credentials[n] = cpi.DirectCredentials(creds)
+ }
+ }
+ }
+ p.cache = cache
+ p.updated = true
+ return nil
+}
+
+func (p *ConsumerProvider) validateCreds(creds cpi.Credentials) error {
+ m := creds.GetProperty(identity.ATTR_AUTHMETH)
+ if m == "" {
+ return errors.ErrRequired(identity.ATTR_AUTHMETH)
+ }
+ meth := methods.Get(m)
+ if meth == nil {
+ return errors.ErrInvalid(identity.ATTR_AUTHMETH, m)
+ }
+ return meth.Validate(creds)
+}
+
+func (p *ConsumerProvider) getToken(ctx context.Context, client *vault.Client, creds cpi.Credentials) (string, error) {
+ m := creds.GetProperty(identity.ATTR_AUTHMETH)
+ return methods.Get(m).GetToken(ctx, client, p.repository.spec.Namespace, creds)
+}
+
+func (p *ConsumerProvider) error(err error, msg string, secret string, keypairs ...interface{}) {
+ if err == nil {
+ return
+ }
+ f := log.Info
+ var v *vault.ResponseError
+ if errors.As(err, &v) && v.StatusCode != http.StatusNotFound {
+ f = log.Error
+ }
+ f(msg, append(keypairs,
+ "server", p.repository.spec.ServerURL,
+ "namespace", p.repository.spec.Namespace,
+ "engine", p.repository.spec.MountPath,
+ "path", path.Join(p.repository.spec.Path, secret),
+ "error", err.Error(),
+ )...,
+ )
+}
+
+func (p *ConsumerProvider) read(ctx context.Context, client *vault.Client, secret string) (common.Properties, common.Properties, []string, error) {
+ // read the secret
+
+ secret = path.Join(p.repository.spec.Path, secret)
+ s, err := client.Secrets.KvV2Read(ctx, secret,
+ vault.WithMountPath(p.repository.spec.MountPath))
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ var id common.Properties
+ var list []string
+ props := getProps(s.Data.Data)
+
+ if meta, ok := s.Data.Metadata["custom_metadata"].(map[string]interface{}); ok {
+ sub := false
+ if cid := meta[CUSTOM_CONSUMERID]; cid != nil {
+ id = common.Properties{}
+ if err := json.Unmarshal([]byte(cid.(string)), &id); err != nil {
+ id = nil
+ }
+ sub = true
+ }
+ if cid := meta[CUSTOM_SECRETS]; cid != nil {
+ if s, ok := meta[CUSTOM_SECRETS].(string); ok {
+ for _, e := range strings.Split(s, ",") {
+ e = strings.TrimSpace(e)
+ if e != "" {
+ list = append(list, e)
+ }
+ }
+ }
+ sub = true
+ }
+ if _, ok := meta[cpi.ID_TYPE]; !sub && ok {
+ id = getProps(meta)
+ }
+ }
+ return props, id, list, nil
+}
+
+func getProps(data map[string]interface{}) common.Properties {
+ props := common.Properties{}
+ for k, v := range data {
+ if s, ok := v.(string); ok {
+ props[k] = s
+ }
+ }
+ return props
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// ConsumerProvider interface
+
+func (p *ConsumerProvider) Unregister(id cpi.ProviderIdentity) {
+}
+
+func (p *ConsumerProvider) Match(ectx cpi.EvaluationContext, req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ return p.get(ectx, req, cur, m)
+}
+
+func (p *ConsumerProvider) Get(req cpi.ConsumerIdentity) (cpi.CredentialsSource, bool) {
+ creds, _ := p.get(nil, req, nil, cpi.CompleteMatch)
+ return creds, creds != nil
+}
+
+func (p *ConsumerProvider) get(ectx cpi.EvaluationContext, req cpi.ConsumerIdentity, cur cpi.ConsumerIdentity, m cpi.IdentityMatcher) (cpi.CredentialsSource, cpi.ConsumerIdentity) {
+ if req.Equals(p.repository.id) {
+ return nil, cur
+ }
+
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ err := p.update(ectx)
+ if err != nil {
+ log.Info("error accessing credentials provider", "error", err)
+ }
+
+ var creds cpi.CredentialsSource
+
+ for _, a := range p.cache.consumer {
+ if m(req, cur, a.Id) {
+ cur = a.Id
+ creds = p.cache.credentials[a.Name]
+ }
+ }
+ return creds, cur
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// lookup
+
+func (c *ConsumerProvider) ExistsCredentials(name string) (bool, error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ err := c.update(nil)
+ if err != nil {
+ return false, err
+ }
+ _, ok := c.cache.credentials[name]
+ return ok, nil
+}
+
+func (c *ConsumerProvider) LookupCredentials(name string) (cpi.Credentials, error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ err := c.update(nil)
+ if err != nil {
+ return nil, err
+ }
+ src, ok := c.cache.credentials[name]
+ if ok {
+ return src, nil
+ }
+ return nil, nil
+}
diff --git a/pkg/contexts/credentials/repositories/vault/repo_int_test.go b/api/credentials/extensions/repositories/vault/repo_int_test.go
similarity index 97%
rename from pkg/contexts/credentials/repositories/vault/repo_int_test.go
rename to api/credentials/extensions/repositories/vault/repo_int_test.go
index bdabe36c0..62bc34d9e 100644
--- a/pkg/contexts/credentials/repositories/vault/repo_int_test.go
+++ b/api/credentials/extensions/repositories/vault/repo_int_test.go
@@ -16,12 +16,12 @@ import (
"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/contexts/credentials"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/identity/hostpath"
- me "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/vault"
- "github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/vault/identity"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/credentials"
+ me "ocm.software/ocm/api/credentials/extensions/repositories/vault"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
)
type vaultMode string
diff --git a/api/credentials/extensions/repositories/vault/repo_test.go b/api/credentials/extensions/repositories/vault/repo_test.go
new file mode 100644
index 000000000..c1b5b07f3
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/repo_test.go
@@ -0,0 +1,70 @@
+package vault_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ me "ocm.software/ocm/api/credentials/extensions/repositories/vault"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const (
+ VAULT_ADDRESS = "127.0.0.1:8200"
+ VAULT_HTTP_URL = "http://" + VAULT_ADDRESS
+ VAULT_NAMESPACE = "test-namespace"
+ VAULT_MOUNT_PATH = "secret"
+ VAULT_PATH_REPO1 = "mysecrets/repo1"
+ VAULT_PATH_REPO2 = "mysecrets/repo2"
+)
+
+var _ = Describe("", func() {
+ Context("serialization and deserialization", func() {
+ DefaultContext := credentials.New()
+
+ specdata := fmt.Sprintf("{\"type\": %q, \"serverURL\": %q, \"namespace\": %q, \"mountPath\": %q, \"path\": %q, \"secrets\": [\"secret1\", \"secret2\", \"secret3\"], \"propagateConsumerIdentity\": true }", me.Type, "http://"+VAULT_ADDRESS, VAULT_NAMESPACE, VAULT_MOUNT_PATH, VAULT_PATH_REPO1)
+ spec := me.NewRepositorySpec("http://"+VAULT_ADDRESS, me.WithNamespace(VAULT_NAMESPACE), me.WithMountPath(VAULT_MOUNT_PATH), me.WithPath(VAULT_PATH_REPO1), me.WithSecrets("secret1", "secret2", "secret3"), me.WithPropagation())
+
+ specdata2 := fmt.Sprintf("{\"type\": %q, \"serverURL\": %q }", me.Type, "http://"+VAULT_ADDRESS)
+ spec2 := me.NewRepositorySpec("http://" + VAULT_ADDRESS)
+
+ It("serializes repo spec", func() {
+ data := Must(json.Marshal(spec))
+ Expect(data).To(YAMLEqual([]byte(specdata)))
+
+ data = Must(json.Marshal(spec2))
+ Expect(data).To(YAMLEqual([]byte(specdata2)))
+ })
+
+ It("deserializes repo spec", func() {
+ localspec := Must(DefaultContext.RepositorySpecForConfig([]byte(specdata), nil))
+ Expect(reflect.TypeOf(localspec).String()).To(Equal("*vault.RepositorySpec"))
+ Expect(localspec).To(Equal(spec))
+
+ localspec = Must(DefaultContext.RepositorySpecForConfig([]byte(specdata2), nil))
+ Expect(reflect.TypeOf(localspec).String()).To(Equal("*vault.RepositorySpec"))
+ Expect(localspec).To(Equal(spec2))
+ })
+
+ It("resolves repository", func() {
+ // Since vault always requires credentials to be accessed, RepositoryForConfig checks whether credentials
+ // for a corresponding consumer exist. Thus, creating such credentials is required to test the method even
+ // though they are not used
+ consumerId := Must(identity.GetConsumerId(VAULT_HTTP_URL, VAULT_NAMESPACE, VAULT_MOUNT_PATH, VAULT_PATH_REPO1))
+ creds := credentials.NewCredentials(common.Properties{
+ identity.ATTR_AUTHMETH: identity.AUTH_TOKEN,
+ identity.ATTR_TOKEN: "token",
+ })
+ DefaultContext.SetCredentialsForConsumer(consumerId, creds)
+
+ repo := Must(DefaultContext.RepositoryForConfig([]byte(specdata), nil))
+ Expect(repo).ToNot(BeNil())
+ })
+ })
+})
diff --git a/api/credentials/extensions/repositories/vault/repository.go b/api/credentials/extensions/repositories/vault/repository.go
new file mode 100644
index 000000000..045ab850d
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/repository.go
@@ -0,0 +1,66 @@
+package vault
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ "ocm.software/ocm/api/credentials/internal"
+)
+
+type Repository struct {
+ ctx cpi.Context
+ spec *RepositorySpec
+ id cpi.ConsumerIdentity
+ provider *ConsumerProvider
+}
+
+var (
+ _ cpi.Repository = (*Repository)(nil)
+ _ cpi.ConsumerIdentityProvider = (*Repository)(nil)
+)
+
+func NewRepository(ctx cpi.Context, spec *RepositorySpec) (*Repository, error) {
+ id, err := identity.GetConsumerId(spec.ServerURL, spec.Namespace, spec.MountPath, spec.Path)
+ if err != nil {
+ return nil, err
+ }
+ r := &Repository{
+ ctx: ctx,
+ spec: spec,
+ id: id,
+ }
+ if spec.ServerURL == "" {
+ return nil, errors.ErrInvalid("server url")
+ }
+ r.provider, err = NewConsumerProvider(r)
+ if err != nil {
+ return nil, err
+ }
+ if spec.PropgateConsumerIdentity {
+ ctx.RegisterConsumerProvider(spec.GetKey(), r.provider)
+ }
+ return r, err
+}
+
+var _ cpi.Repository = &Repository{}
+
+func (r *Repository) ExistsCredentials(name string) (bool, error) {
+ return r.provider.ExistsCredentials(name)
+}
+
+func (r *Repository) LookupCredentials(name string) (cpi.Credentials, error) {
+ return r.provider.LookupCredentials(name)
+}
+
+func (r *Repository) WriteCredentials(name string, creds cpi.Credentials) (cpi.Credentials, error) {
+ return nil, errors.ErrNotSupported("write", "credentials", Type)
+}
+
+func (r *Repository) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
+ return r.id
+}
+
+func (r *Repository) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
diff --git a/pkg/contexts/credentials/repositories/vault/suite_test.go b/api/credentials/extensions/repositories/vault/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/repositories/vault/suite_test.go
rename to api/credentials/extensions/repositories/vault/suite_test.go
diff --git a/api/credentials/extensions/repositories/vault/type.go b/api/credentials/extensions/repositories/vault/type.go
new file mode 100644
index 000000000..f0512a180
--- /dev/null
+++ b/api/credentials/extensions/repositories/vault/type.go
@@ -0,0 +1,82 @@
+package vault
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/mandelsoft/goutils/optionutils"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/extensions/repositories/vault/identity"
+ "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "HashiCorpVault"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1, cpi.WithDescription(usage), cpi.WithFormatSpec(format)))
+}
+
+// RepositorySpec describes a docker config based credential repository interface.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ ServerURL string `json:"serverURL"`
+ Options `json:",inline"`
+}
+
+var _ cpi.ConsumerIdentityProvider = (*RepositorySpec)(nil)
+
+// NewRepositorySpec creates a new memory RepositorySpec.
+func NewRepositorySpec(url string, opts ...Option) *RepositorySpec {
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ ServerURL: url,
+ Options: *optionutils.EvalOptions(opts...),
+ }
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds cpi.Credentials) (cpi.Repository, error) {
+ r := ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, newRepositories)
+ repos, ok := r.(*Repositories)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to Repositories", r)
+ }
+ spec := *a
+ spec.Secrets = slices.Clone(a.Secrets)
+ if spec.MountPath == "" {
+ spec.MountPath = "secret"
+ }
+ return repos.GetRepository(ctx, &spec)
+}
+
+func (a *RepositorySpec) GetKey() cpi.ProviderIdentity {
+ spec := *a
+ spec.PropgateConsumerIdentity = false
+ data, err := json.Marshal(&spec)
+ if err == nil {
+ return cpi.ProviderIdentity(data)
+ }
+ return cpi.ProviderIdentity(spec.ServerURL)
+}
+
+func (a *RepositorySpec) GetConsumerId(uctx ...internal.UsageContext) internal.ConsumerIdentity {
+ id, err := identity.GetConsumerId(a.ServerURL, a.Namespace, a.MountPath, a.Path)
+ if err != nil {
+ return nil
+ }
+ return id
+}
+
+func (a *RepositorySpec) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
diff --git a/api/credentials/gc_test.go b/api/credentials/gc_test.go
new file mode 100644
index 000000000..260c1164a
--- /dev/null
+++ b/api/credentials/gc_test.go
@@ -0,0 +1,34 @@
+package credentials_test
+
+import (
+ "runtime"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ me "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+var _ = Describe("area test", func() {
+ It("can be garbage collected", func() {
+ ctx := me.New()
+
+ r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx)
+ Expect(r).NotTo(BeNil())
+
+ runtime.GC()
+ time.Sleep(time.Second)
+ ctx.GetType()
+ Expect(r.Get()).To(BeNil())
+
+ ctx = nil
+ for i := 0; i < 100; i++ {
+ runtime.GC()
+ time.Sleep(time.Millisecond)
+ }
+
+ Expect(r.Get()).To(ContainElement(ContainSubstring(me.CONTEXT_TYPE)))
+ })
+})
diff --git a/api/credentials/identity/hostpath/id_test.go b/api/credentials/identity/hostpath/id_test.go
new file mode 100644
index 000000000..eca34ddc1
--- /dev/null
+++ b/api/credentials/identity/hostpath/id_test.go
@@ -0,0 +1,204 @@
+package hostpath_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return hostpath.IdentityMatcher("OCIRegistry")(pattern, cur, id)
+}
+
+var _ = Describe("ctf management", func() {
+ Context("with path", func() {
+ pat := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("path prefix", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("longer prefix", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b/c",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing path", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+
+ Expect(IdentityMatcher(id, nil, pat)).To(BeTrue()) // accept additional port as fallback
+ Expect(IdentityMatcher(id, id, pat)).To(BeFalse()) // but not to replace more general match
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "0815",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+
+ It("different host", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "other",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("no host", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(id, nil, pat)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, id, pat)).To(BeTrue())
+ })
+
+ It("different scheme", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "otherscheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("no scheme", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PATHPREFIX: "a/b",
+ hostpath.ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(id, nil, pat)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, id, pat)).To(BeTrue())
+ })
+ })
+
+ Context("without path", func() {
+ pat := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+
+ It("complete", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, id, id)).To(BeFalse())
+ })
+
+ It("different prefix", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_PATHPREFIX: "b",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("missing port", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("different port", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "0815",
+ hostpath.ID_SCHEME: "scheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+
+ It("different scheme", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ hostpath.ID_SCHEME: "otherscheme://",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeFalse())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ It("no scheme", func() {
+ id := credentials.ConsumerIdentity{
+ hostpath.ID_HOSTNAME: "host",
+ hostpath.ID_PORT: "4711",
+ }
+ Expect(IdentityMatcher(pat, nil, id)).To(BeTrue())
+ Expect(IdentityMatcher(pat, pat, id)).To(BeFalse())
+ })
+ })
+})
diff --git a/api/credentials/identity/hostpath/identity.go b/api/credentials/identity/hostpath/identity.go
new file mode 100644
index 000000000..4a0369ced
--- /dev/null
+++ b/api/credentials/identity/hostpath/identity.go
@@ -0,0 +1,148 @@
+package hostpath
+
+import (
+ "net/url"
+ "strings"
+
+ "ocm.software/ocm/api/credentials/cpi"
+)
+
+// IDENTITY_TYPE is the identity of this matcher.
+const IDENTITY_TYPE = "hostpath"
+
+// ID_TYPE is the type of the consumer.
+const ID_TYPE = cpi.ID_TYPE
+
+// ID_HOSTNAME is a hostname.
+const ID_HOSTNAME = "hostname"
+
+// ID_PORT is a port.
+const ID_PORT = "port"
+
+// ID_PATHPREFIX is the path prefix below the host.
+const ID_PATHPREFIX = "pathprefix"
+
+// ID_SCHEME is the scheme prefix.
+const ID_SCHEME = "scheme"
+
+func init() {
+ cpi.RegisterStandardIdentityMatcher(IDENTITY_TYPE, Matcher, `Host and path based credential matcher
+
+This matcher works on the following properties:
+
+- *`+ID_TYPE+`
* (required if set in pattern): the identity type
+- *`+ID_HOSTNAME+`
* (required if set in pattern): the hostname of a server
+- *`+ID_SCHEME+`
* (optional): the URL scheme of a server
+- *`+ID_PORT+`
* (optional): the port of a server
+- *`+ID_PATHPREFIX+`
* (optional): a path prefix to match. The
+ element with the most matching path components is selected (separator is /
).
+`)
+}
+
+var Matcher = IdentityMatcher("")
+
+func Match(identityType string, request, cur, id cpi.ConsumerIdentity) (match bool, better bool) {
+ if request[ID_TYPE] != "" && request[ID_TYPE] != id[ID_TYPE] {
+ return false, false
+ }
+
+ if identityType != "" && request[ID_TYPE] != "" && identityType != request[ID_TYPE] {
+ return false, false
+ }
+
+ if request[ID_HOSTNAME] != "" && id[ID_HOSTNAME] != "" && request[ID_HOSTNAME] != id[ID_HOSTNAME] {
+ return false, false
+ }
+
+ if request[ID_PORT] != "" {
+ if id[ID_PORT] != "" && id[ID_PORT] != request[ID_PORT] {
+ return false, false
+ }
+ }
+
+ if request[ID_SCHEME] != "" {
+ if id[ID_SCHEME] != "" && id[ID_SCHEME] != request[ID_SCHEME] {
+ return false, false
+ }
+ }
+
+ if request[ID_PATHPREFIX] != "" {
+ if id[ID_PATHPREFIX] != "" {
+ if len(id[ID_PATHPREFIX]) > len(request[ID_PATHPREFIX]) {
+ return false, false
+ }
+ pcomps := strings.Split(request[ID_PATHPREFIX], "/")
+ icomps := strings.Split(id[ID_PATHPREFIX], "/")
+ if len(icomps) > len(pcomps) {
+ return false, false
+ }
+ for i := range icomps {
+ if pcomps[i] != icomps[i] {
+ return false, false
+ }
+ }
+ }
+ } else {
+ if id[ID_PATHPREFIX] != "" {
+ return false, false
+ }
+ }
+
+ // ok now it basically matches, check against current match
+ if len(cur) == 0 {
+ return true, true
+ }
+
+ if cur[ID_HOSTNAME] == "" && id[ID_HOSTNAME] != "" {
+ return true, true
+ }
+ if cur[ID_PORT] == "" && (id[ID_PORT] != "" && request[ID_PORT] != "") {
+ return true, true
+ }
+ if cur[ID_SCHEME] == "" && (id[ID_SCHEME] != "" && request[ID_SCHEME] != "") {
+ return true, true
+ }
+
+ if len(cur[ID_PATHPREFIX]) < len(id[ID_PATHPREFIX]) {
+ return true, true
+ }
+ return true, false
+}
+
+func IdentityMatcher(identityType string) cpi.IdentityMatcher {
+ return func(request, cur, id cpi.ConsumerIdentity) bool {
+ _, better := Match(identityType, request, cur, id)
+ return better
+ }
+}
+
+func GetConsumerIdentity(typ, _url string) cpi.ConsumerIdentity {
+ u, err := url.Parse(_url)
+ if err != nil {
+ return nil
+ }
+
+ id := cpi.NewConsumerIdentity(typ)
+ if u.Host != "" {
+ parts := strings.Split(u.Host, ":")
+ if len(parts) > 1 {
+ id[ID_PORT] = parts[1]
+ } else {
+ switch u.Scheme {
+ case "https":
+ id[ID_PORT] = "443"
+ case "http":
+ id[ID_PORT] = "80"
+ }
+ }
+ id[ID_HOSTNAME] = parts[0]
+ }
+ if u.Scheme != "" {
+ id[ID_SCHEME] = u.Scheme
+ }
+ path := strings.Trim(u.Path, "/")
+ if path != "" {
+ id[ID_PATHPREFIX] = path
+ }
+ return id
+}
diff --git a/pkg/contexts/credentials/identity/hostpath/suite_test.go b/api/credentials/identity/hostpath/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/identity/hostpath/suite_test.go
rename to api/credentials/identity/hostpath/suite_test.go
diff --git a/api/credentials/init.go b/api/credentials/init.go
new file mode 100644
index 000000000..6c8a0fb88
--- /dev/null
+++ b/api/credentials/init.go
@@ -0,0 +1,7 @@
+package credentials
+
+import (
+ _ "ocm.software/ocm/api/credentials/builtin"
+ _ "ocm.software/ocm/api/credentials/config"
+ _ "ocm.software/ocm/api/credentials/extensions/repositories"
+)
diff --git a/api/credentials/interface.go b/api/credentials/interface.go
new file mode 100644
index 000000000..b0aa3f204
--- /dev/null
+++ b/api/credentials/interface.go
@@ -0,0 +1,134 @@
+package credentials
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/credentials/extensions/repositories/directcreds"
+ "ocm.software/ocm/api/credentials/internal"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ KIND_CREDENTIALS = internal.KIND_CREDENTIALS
+ KIND_CONSUMER = internal.KIND_CONSUMER
+ KIND_REPOSITORY = internal.KIND_REPOSITORY
+)
+
+const CONTEXT_TYPE = internal.CONTEXT_TYPE
+
+const AliasRepositoryType = internal.AliasRepositoryType
+
+type (
+ Context = internal.Context
+ ContextProvider = internal.ContextProvider
+ RepositoryTypeScheme = internal.RepositoryTypeScheme
+ Repository = internal.Repository
+ Credentials = internal.Credentials
+ CredentialsSource = internal.CredentialsSource
+ CredentialsChain = internal.CredentialsChain
+ CredentialsSpec = internal.CredentialsSpec
+ RepositorySpec = internal.RepositorySpec
+)
+
+type (
+ ConsumerIdentity = internal.ConsumerIdentity
+ ConsumerIdentityProvider = internal.ConsumerIdentityProvider
+ ProviderIdentity = internal.ProviderIdentity
+ UsageContext = internal.UsageContext
+ StringUsageContext = internal.StringUsageContext
+ IdentityMatcher = internal.IdentityMatcher
+ IdentityMatcherInfo = internal.IdentityMatcherInfo
+ IdentityMatcherInfos = internal.IdentityMatcherInfos
+ IdentityMatcherRegistry = internal.IdentityMatcherRegistry
+)
+
+type (
+ GenericRepositorySpec = internal.GenericRepositorySpec
+ GenericCredentialsSpec = internal.GenericCredentialsSpec
+ DirectCredentials = internal.DirectCredentials
+)
+
+func DefaultContext() internal.Context {
+ return internal.DefaultContext
+}
+
+func FromContext(ctx context.Context) Context {
+ return internal.FromContext(ctx)
+}
+
+func FromProvider(p ContextProvider) Context {
+ return internal.FromProvider(p)
+}
+
+func DefinedForContext(ctx context.Context) (Context, bool) {
+ return internal.DefinedForContext(ctx)
+}
+
+func NewCredentialsSpec(name string, repospec RepositorySpec) CredentialsSpec {
+ return internal.NewCredentialsSpec(name, repospec)
+}
+
+func NewGenericCredentialsSpec(name string, repospec *GenericRepositorySpec) CredentialsSpec {
+ return internal.NewGenericCredentialsSpec(name, repospec)
+}
+
+func NewGenericRepositorySpec(data []byte, unmarshaler runtime.Unmarshaler) (RepositorySpec, error) {
+ return internal.NewGenericRepositorySpec(data, unmarshaler)
+}
+
+func NewCredentials(props common.Properties) Credentials {
+ return internal.NewCredentials(props)
+}
+
+func CredentialsFromList(props ...string) Credentials {
+ creds := DirectCredentials{}
+ for i := 1; i < len(props); i += 2 {
+ creds[props[i-1]] = props[i]
+ }
+ return creds
+}
+
+func CredentialsSpecFromList(props ...string) CredentialsSpec {
+ creds := DirectCredentials{}
+ for i := 1; i < len(props); i += 2 {
+ creds[props[i-1]] = props[i]
+ }
+ return directcreds.NewCredentials(creds.Properties())
+}
+
+func ToGenericCredentialsSpec(spec CredentialsSpec) (*GenericCredentialsSpec, error) {
+ return internal.ToGenericCredentialsSpec(spec)
+}
+
+func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error) {
+ return internal.ToGenericRepositorySpec(spec)
+}
+
+func ErrUnknownCredentials(name string) error {
+ return internal.ErrUnknownCredentials(name)
+}
+
+// CredentialsForConsumer determine effective credentials for a consumer.
+// If no credentials are configured no error and nil is returned.
+// It evaluates a found credentials source for the consumer to determine the
+// final credential properties.
+func CredentialsForConsumer(ctx ContextProvider, id ConsumerIdentity, matchers ...IdentityMatcher) (Credentials, error) {
+ return internal.CredentialsForConsumer(ctx, id, false, matchers...)
+}
+
+// RequiredCredentialsForConsumer like CredentialsForConsumer, but an errors is returned
+// if no credentials are found.
+func RequiredCredentialsForConsumer(ctx ContextProvider, id ConsumerIdentity, matchers ...IdentityMatcher) (Credentials, error) {
+ return internal.CredentialsForConsumer(ctx, id, true, matchers...)
+}
+
+var (
+ CompleteMatch = internal.CompleteMatch
+ NoMatch = internal.NoMatch
+ PartialMatch = internal.PartialMatch
+)
+
+func NewConsumerIdentity(typ string, attrs ...string) ConsumerIdentity {
+ return internal.NewConsumerIdentity(typ, attrs...)
+}
diff --git a/api/credentials/internal/builder.go b/api/credentials/internal/builder.go
new file mode 100644
index 000000000..a5940f228
--- /dev/null
+++ b/api/credentials/internal/builder.go
@@ -0,0 +1,79 @@
+package internal
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/datacontext"
+)
+
+type Builder struct {
+ ctx context.Context
+ config config.Context
+ reposcheme RepositoryTypeScheme
+ matchers IdentityMatcherRegistry
+}
+
+func (b *Builder) getContext() context.Context {
+ if b.ctx == nil {
+ return context.Background()
+ }
+ return b.ctx
+}
+
+func (b Builder) WithContext(ctx context.Context) Builder {
+ b.ctx = ctx
+ return b
+}
+
+func (b Builder) WithConfig(ctx config.Context) Builder {
+ b.config = ctx
+ return b
+}
+
+func (b Builder) WithRepositoyTypeScheme(scheme RepositoryTypeScheme) Builder {
+ b.reposcheme = scheme
+ return b
+}
+
+func (b Builder) WithStandardConumerMatchers(matchers IdentityMatcherRegistry) Builder {
+ b.matchers = matchers
+ return b
+}
+
+func (b Builder) Bound() (Context, context.Context) {
+ c := b.New()
+ return c, context.WithValue(b.getContext(), key, c)
+}
+
+func (b Builder) New(m ...datacontext.BuilderMode) Context {
+ mode := datacontext.Mode(m...)
+ ctx := b.getContext()
+
+ if b.config == nil {
+ var ok bool
+ b.config, ok = config.DefinedForContext(ctx)
+ if !ok && mode != datacontext.MODE_SHARED {
+ b.config = config.New(mode)
+ }
+ }
+ if b.reposcheme == nil {
+ switch mode {
+ case datacontext.MODE_INITIAL:
+ b.reposcheme = NewRepositoryTypeScheme(nil)
+ case datacontext.MODE_CONFIGURED:
+ b.reposcheme = NewRepositoryTypeScheme(nil)
+ b.reposcheme.AddKnownTypes(DefaultRepositoryTypeScheme)
+ case datacontext.MODE_EXTENDED:
+ b.reposcheme = NewRepositoryTypeScheme(nil, DefaultRepositoryTypeScheme)
+ case datacontext.MODE_DEFAULTED:
+ fallthrough
+ case datacontext.MODE_SHARED:
+ b.reposcheme = DefaultRepositoryTypeScheme
+ }
+ }
+ if b.matchers == nil {
+ b.matchers = StandardIdentityMatchers
+ }
+ return datacontext.SetupContext(mode, newContext(b.config, b.reposcheme, b.matchers, b.config))
+}
diff --git a/api/credentials/internal/builder_test.go b/api/credentials/internal/builder_test.go
new file mode 100644
index 000000000..3f37c6b39
--- /dev/null
+++ b/api/credentials/internal/builder_test.go
@@ -0,0 +1,58 @@
+package internal_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ local "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/datacontext"
+)
+
+var _ = Describe("builder test", func() {
+ It("creates local", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_SHARED)
+
+ Expect(ctx.AttributesContext()).To(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.RepositoryTypes()).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme))
+
+ Expect(ctx.ConfigContext().GetId()).To(BeIdenticalTo(config.DefaultContext().GetId()))
+ })
+
+ It("creates defaulted", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_DEFAULTED)
+
+ Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.RepositoryTypes()).To(BeIdenticalTo(local.DefaultRepositoryTypeScheme))
+
+ Expect(ctx.ConfigContext().GetId()).NotTo(BeIdenticalTo(config.DefaultContext().GetType()))
+ Expect(ctx.ConfigContext().ConfigTypes()).To(BeIdenticalTo(config.DefaultContext().ConfigTypes()))
+ })
+
+ It("creates configured", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_CONFIGURED)
+
+ Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.RepositoryTypes()).NotTo(BeIdenticalTo(local.DefaultRepositoryTypeScheme))
+ Expect(ctx.RepositoryTypes().KnownTypeNames()).To(Equal(local.DefaultRepositoryTypeScheme.KnownTypeNames()))
+
+ Expect(ctx.ConfigContext().GetId()).NotTo(BeIdenticalTo(config.DefaultContext().GetId()))
+ Expect(ctx.ConfigContext().ConfigTypes()).NotTo(BeIdenticalTo(config.DefaultContext().ConfigTypes()))
+ Expect(ctx.ConfigContext().ConfigTypes().KnownTypeNames()).To(Equal(config.DefaultContext().ConfigTypes().KnownTypeNames()))
+ })
+
+ It("creates iniial", func() {
+ ctx := local.Builder{}.New(datacontext.MODE_INITIAL)
+
+ Expect(ctx.AttributesContext()).NotTo(BeIdenticalTo(datacontext.DefaultContext))
+ Expect(ctx).NotTo(BeIdenticalTo(local.DefaultContext))
+ Expect(ctx.RepositoryTypes()).NotTo(BeIdenticalTo(local.DefaultRepositoryTypeScheme))
+ Expect(len(ctx.RepositoryTypes().KnownTypeNames())).To(Equal(0))
+
+ Expect(ctx.ConfigContext()).NotTo(BeIdenticalTo(config.DefaultContext()))
+ Expect(len(ctx.ConfigContext().ConfigTypes().KnownTypeNames())).To(Equal(0))
+ })
+})
diff --git a/pkg/contexts/credentials/internal/builtin.go b/api/credentials/internal/builtin.go
similarity index 100%
rename from pkg/contexts/credentials/internal/builtin.go
rename to api/credentials/internal/builtin.go
diff --git a/pkg/contexts/credentials/internal/const.go b/api/credentials/internal/const.go
similarity index 100%
rename from pkg/contexts/credentials/internal/const.go
rename to api/credentials/internal/const.go
diff --git a/pkg/contexts/credentials/internal/consumers.go b/api/credentials/internal/consumers.go
similarity index 100%
rename from pkg/contexts/credentials/internal/consumers.go
rename to api/credentials/internal/consumers.go
diff --git a/api/credentials/internal/context.go b/api/credentials/internal/context.go
new file mode 100644
index 000000000..333b9e1d1
--- /dev/null
+++ b/api/credentials/internal/context.go
@@ -0,0 +1,322 @@
+package internal
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/maputils"
+ "golang.org/x/exp/maps"
+
+ "ocm.software/ocm/api/config"
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+// CONTEXT_TYPE is the global type for a credential context.
+const CONTEXT_TYPE = "credentials" + datacontext.OCM_CONTEXT_SUFFIX
+
+// ProviderIdentity is used to uniquely identify a provider
+// for a configured consumer id. If non-empty it
+// must start with a DNSname identifying the origin of the
+// provider followed by a slash and a local arbitrary identity.
+type ProviderIdentity = runtimefinalizer.ObjectIdentity
+
+type ContextProvider interface {
+ CredentialsContext() Context
+}
+
+type ConsumerProvider interface {
+ Unregister(id ProviderIdentity)
+ Get(id ConsumerIdentity) (CredentialsSource, bool)
+ Match(ectx EvaluationContext, id ConsumerIdentity, cur ConsumerIdentity, matcher IdentityMatcher) (CredentialsSource, ConsumerIdentity)
+}
+
+type EvaluationContext *evaluationContext
+
+type evaluationContext struct {
+ data map[reflect.Type]interface{}
+}
+
+func (e evaluationContext) String() string {
+ return fmt.Sprintf("%v", maputils.Transform(e.data, func(k reflect.Type, v interface{}) (string, string) {
+ return k.Name(), fmt.Sprintf("%v", v)
+ }))
+}
+
+func GetEvaluationContextFor[T any](ectx EvaluationContext) T {
+ var _nil T
+ if ectx.data == nil {
+ return _nil
+ }
+ return generics.Cast[T](ectx.data[generics.TypeOf[T]()])
+}
+
+func SetEvaluationContextFor(ectx EvaluationContext, e any) EvaluationContext {
+ if ectx.data == nil {
+ ectx.data = map[reflect.Type]interface{}{}
+ }
+ n := &evaluationContext{maps.Clone(ectx.data)}
+ n.data[reflect.TypeOf(e)] = e
+ return n
+}
+
+type Context interface {
+ datacontext.Context
+ ContextProvider
+ config.ContextProvider
+
+ AttributesContext() datacontext.AttributesContext
+ RepositoryTypes() RepositoryTypeScheme
+
+ RepositorySpecForConfig(data []byte, unmarshaler runtime.Unmarshaler) (RepositorySpec, error)
+
+ RepositoryForSpec(spec RepositorySpec, creds ...CredentialsSource) (Repository, error)
+ RepositoryForConfig(data []byte, unmarshaler runtime.Unmarshaler, creds ...CredentialsSource) (Repository, error)
+
+ CredentialsForSpec(spec CredentialsSpec, creds ...CredentialsSource) (Credentials, error)
+ CredentialsForConfig(data []byte, unmarshaler runtime.Unmarshaler, cred ...CredentialsSource) (Credentials, error)
+
+ RegisterConsumerProvider(id ProviderIdentity, provider ConsumerProvider)
+ UnregisterConsumerProvider(id ProviderIdentity)
+
+ GetCredentialsForConsumer(ConsumerIdentity, ...IdentityMatcher) (CredentialsSource, error)
+ getCredentialsForConsumer(EvaluationContext, ConsumerIdentity, ...IdentityMatcher) (CredentialsSource, error)
+ SetCredentialsForConsumer(identity ConsumerIdentity, creds CredentialsSource)
+ SetCredentialsForConsumerWithProvider(pid ProviderIdentity, identity ConsumerIdentity, creds CredentialsSource)
+
+ SetAlias(name string, spec RepositorySpec, creds ...CredentialsSource) error
+
+ ConsumerIdentityMatchers() IdentityMatcherRegistry
+}
+
+var key = reflect.TypeOf(_context{})
+
+// DefaultContext is the default context initialized by init functions.
+var DefaultContext = Builder{}.New(datacontext.MODE_SHARED)
+
+// FromContext returns the Context to use for context.Context.
+// This is either an explicit context or the default context.
+func FromContext(ctx context.Context) Context {
+ c, _ := datacontext.ForContextByKey(ctx, key, DefaultContext)
+ return c.(Context)
+}
+
+func FromProvider(p ContextProvider) Context {
+ if p == nil {
+ return nil
+ }
+ return p.CredentialsContext()
+}
+
+func DefinedForContext(ctx context.Context) (Context, bool) {
+ c, ok := datacontext.ForContextByKey(ctx, key, DefaultContext)
+ if c != nil {
+ return c.(Context), ok
+ }
+ return nil, ok
+}
+
+type _InternalContext = datacontext.InternalContext
+
+type _context struct {
+ _InternalContext
+
+ sharedattributes datacontext.AttributesContext
+ updater cfgcpi.Updater
+ knownRepositoryTypes RepositoryTypeScheme
+ consumerIdentityMatchers IdentityMatcherRegistry
+ consumerProviders *consumerProviderRegistry
+}
+
+var (
+ _ Context = (*_context)(nil)
+ _ datacontext.ViewCreator[Context] = (*_context)(nil)
+)
+
+// gcWrapper is used as garbage collectable
+// wrapper for a context implementation
+// to establish a runtime finalizer.
+type gcWrapper struct {
+ datacontext.GCWrapper
+ *_context
+}
+
+func newView(c *_context, ref ...bool) Context {
+ if utils.Optional(ref...) {
+ return datacontext.FinalizedContext[gcWrapper](c)
+ }
+ return c
+}
+
+func (w *gcWrapper) SetContext(c *_context) {
+ w._context = c
+}
+
+func newContext(configctx config.Context, reposcheme RepositoryTypeScheme, consumerMatchers IdentityMatcherRegistry, delegates datacontext.Delegates) Context {
+ c := &_context{
+ sharedattributes: datacontext.PersistentContextRef(configctx.AttributesContext()),
+ knownRepositoryTypes: reposcheme,
+ consumerIdentityMatchers: consumerMatchers,
+ consumerProviders: newConsumerProviderRegistry(),
+ }
+ c._InternalContext = datacontext.NewContextBase(c, CONTEXT_TYPE, key, configctx.GetAttributes(), delegates)
+ c.updater = cfgcpi.NewUpdaterForFactory(datacontext.PersistentContextRef(configctx), c.CredentialsContext)
+ return newView(c, true)
+}
+
+func (c *_context) CreateView() Context {
+ return newView(c, true)
+}
+
+func (c *_context) CredentialsContext() Context {
+ return newView(c)
+}
+
+func (c *_context) Update() error {
+ return c.updater.Update()
+}
+
+func (c *_context) GetType() string {
+ return CONTEXT_TYPE
+}
+
+func (c *_context) AttributesContext() datacontext.AttributesContext {
+ return c.sharedattributes
+}
+
+func (c *_context) ConfigContext() config.Context {
+ return c.updater.GetContext()
+}
+
+func (c *_context) RepositoryTypes() RepositoryTypeScheme {
+ return c.knownRepositoryTypes
+}
+
+func (c *_context) RepositorySpecForConfig(data []byte, unmarshaler runtime.Unmarshaler) (RepositorySpec, error) {
+ return c.knownRepositoryTypes.Decode(data, unmarshaler)
+}
+
+func (c *_context) RepositoryForSpec(spec RepositorySpec, creds ...CredentialsSource) (Repository, error) {
+ out := newView(c)
+ cred, err := CredentialsChain(creds).Credentials(out)
+ if err != nil {
+ return nil, err
+ }
+ c.Update()
+ return spec.Repository(out, cred)
+}
+
+func (c *_context) RepositoryForConfig(data []byte, unmarshaler runtime.Unmarshaler, creds ...CredentialsSource) (Repository, error) {
+ spec, err := c.knownRepositoryTypes.Decode(data, unmarshaler)
+ if err != nil {
+ return nil, err
+ }
+ return c.RepositoryForSpec(spec, creds...)
+}
+
+func (c *_context) CredentialsForSpec(spec CredentialsSpec, creds ...CredentialsSource) (Credentials, error) {
+ out := newView(c)
+ repospec := spec.GetRepositorySpec(out)
+ repo, err := c.RepositoryForSpec(repospec, creds...)
+ if err != nil {
+ return nil, err
+ }
+ return repo.LookupCredentials(spec.GetCredentialsName())
+}
+
+func (c *_context) CredentialsForConfig(data []byte, unmarshaler runtime.Unmarshaler, creds ...CredentialsSource) (Credentials, error) {
+ spec := &GenericCredentialsSpec{}
+ err := unmarshaler.Unmarshal(data, spec)
+ if err != nil {
+ return nil, err
+ }
+ return c.CredentialsForSpec(spec, creds...)
+}
+
+var emptyIdentity = ConsumerIdentity{}
+
+func (c *_context) GetCredentialsForConsumer(identity ConsumerIdentity, matchers ...IdentityMatcher) (CredentialsSource, error) {
+ return c.getCredentialsForConsumer(nil, identity, matchers...)
+}
+
+func (c *_context) getCredentialsForConsumer(ectx EvaluationContext, identity ConsumerIdentity, matchers ...IdentityMatcher) (CredentialsSource, error) {
+ err := c.Update()
+ if err != nil {
+ return nil, err
+ }
+
+ if ectx == nil {
+ ectx = &evaluationContext{}
+ }
+ m := c.defaultMatcher(identity, matchers...)
+ var credsrc CredentialsSource
+ if m == nil {
+ credsrc, _ = c.consumerProviders.Get(identity)
+ } else {
+ credsrc, _ = c.consumerProviders.Match(ectx, identity, nil, m)
+ }
+ if credsrc == nil {
+ credsrc, _ = c.consumerProviders.Get(emptyIdentity)
+ }
+ if credsrc == nil {
+ return nil, ErrUnknownConsumer(identity.String())
+ }
+ return credsrc, nil
+}
+
+func (c *_context) defaultMatcher(id ConsumerIdentity, matchers ...IdentityMatcher) IdentityMatcher {
+ def := c.consumerIdentityMatchers.Get(id.Type())
+ if def == nil {
+ def = PartialMatch
+ }
+ return mergeMatcher(def, andMatcher, matchers)
+}
+
+func (c *_context) SetCredentialsForConsumer(identity ConsumerIdentity, creds CredentialsSource) {
+ c.Update()
+ c.consumerProviders.Set(identity, "", creds)
+}
+
+func (c *_context) SetCredentialsForConsumerWithProvider(pid ProviderIdentity, identity ConsumerIdentity, creds CredentialsSource) {
+ c.Update()
+ c.consumerProviders.Set(identity, pid, creds)
+}
+
+func (c *_context) ConsumerIdentityMatchers() IdentityMatcherRegistry {
+ return c.consumerIdentityMatchers
+}
+
+func (c *_context) SetAlias(name string, spec RepositorySpec, creds ...CredentialsSource) error {
+ c.Update()
+ t := c.knownRepositoryTypes.GetType(AliasRepositoryType)
+ if t == nil {
+ return errors.ErrNotSupported("aliases")
+ }
+ if a, ok := t.(AliasRegistry); ok {
+ return a.SetAlias(c, name, spec, CredentialsChain(creds))
+ }
+ return errors.ErrNotImplemented("interface", "AliasRegistry", reflect.TypeOf(t).String())
+}
+
+func (c *_context) RegisterConsumerProvider(id ProviderIdentity, provider ConsumerProvider) {
+ c.consumerProviders.Register(id, provider)
+}
+
+func (c *_context) UnregisterConsumerProvider(id ProviderIdentity) {
+ c.consumerProviders.Unregister(id)
+}
+
+///////////////////////////////////////
+
+func GetCredentialsForConsumer(ctx Context, ectx EvaluationContext, identity ConsumerIdentity, matchers ...IdentityMatcher) (CredentialsSource, error) {
+ if ectx == nil {
+ ectx = &evaluationContext{}
+ }
+ return ctx.getCredentialsForConsumer(ectx, identity, matchers...)
+}
diff --git a/api/credentials/internal/cred_test.go b/api/credentials/internal/cred_test.go
new file mode 100644
index 000000000..bf1233986
--- /dev/null
+++ b/api/credentials/internal/cred_test.go
@@ -0,0 +1,87 @@
+package internal_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/extensions/repositories/memory"
+ "ocm.software/ocm/api/credentials/internal"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+var DefaultContext = credentials.New()
+
+var _ = Describe("generic credentials", func() {
+ props := common.Properties{
+ "user": "USER",
+ "password": "PASSWORD",
+ }
+ credmemdata := "{\"credentialsName\":\"cred\",\"repoName\":\"test\",\"type\":\"Memory\"}"
+ memdata := "{\"repoName\":\"test\",\"type\":\"Memory\"}"
+
+ _ = props
+
+ It("de/serializes credentials spec", func() {
+ repospec := memory.NewRepositorySpec("test")
+ credspec := credentials.NewCredentialsSpec("cred", repospec)
+
+ data, err := json.Marshal(credspec)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(credmemdata)))
+
+ credspec = &internal.DefaultCredentialsSpec{}
+ err = json.Unmarshal(data, credspec)
+ Expect(err).To(Succeed())
+ s := credspec.(*internal.DefaultCredentialsSpec)
+ Expect(reflect.TypeOf(s.RepositorySpec).String()).To(Equal("*memory.RepositorySpec"))
+ Expect(s.CredentialsName).To(Equal("cred"))
+ Expect(s.RepositorySpec.(*memory.RepositorySpec).RepositoryName).To(Equal("test"))
+ })
+
+ It("de/serializes generic credentials spec", func() {
+ credspec := &internal.GenericCredentialsSpec{}
+
+ err := json.Unmarshal([]byte(credmemdata), credspec)
+ Expect(err).To(Succeed())
+
+ data, err := json.Marshal(credspec)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(credmemdata)))
+ })
+
+ It("de/serializes generic repository spec", func() {
+ credspec := &internal.GenericRepositorySpec{}
+
+ err := json.Unmarshal([]byte(memdata), credspec)
+ Expect(err).To(Succeed())
+
+ data, err := json.Marshal(credspec)
+ Expect(err).To(Succeed())
+ Expect(data).To(Equal([]byte(memdata)))
+ })
+
+ It("converts credentials spec to generic ones", func() {
+ repospec := memory.NewRepositorySpec("test")
+ credspec := credentials.NewCredentialsSpec("cred", repospec)
+ data, err := json.Marshal(credspec)
+ Expect(err).To(Succeed())
+
+ gen, err := credentials.ToGenericCredentialsSpec(credspec)
+ Expect(err).To(Succeed())
+
+ Expect(reflect.TypeOf(gen).String()).To(Equal("*internal.GenericCredentialsSpec"))
+ Expect(reflect.TypeOf(gen.RepositorySpec).String()).To(Equal("*internal.GenericRepositorySpec"))
+
+ gen2, err := credentials.ToGenericCredentialsSpec(gen)
+ Expect(err).To(Succeed())
+ Expect(gen2).To(BeIdenticalTo(gen))
+
+ data3, err := json.Marshal(gen)
+ Expect(err).To(Succeed())
+ Expect(data3).To(Equal(data))
+ })
+})
diff --git a/pkg/contexts/credentials/internal/credentials.go b/api/credentials/internal/credentials.go
similarity index 100%
rename from pkg/contexts/credentials/internal/credentials.go
rename to api/credentials/internal/credentials.go
diff --git a/pkg/contexts/credentials/internal/credentialsspec.go b/api/credentials/internal/credentialsspec.go
similarity index 98%
rename from pkg/contexts/credentials/internal/credentialsspec.go
rename to api/credentials/internal/credentialsspec.go
index f423c5001..e0e97c39c 100644
--- a/pkg/contexts/credentials/internal/credentialsspec.go
+++ b/api/credentials/internal/credentialsspec.go
@@ -6,7 +6,7 @@ import (
"github.com/modern-go/reflect2"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/utils/runtime"
)
// CredentialsSpec describes a dedicated credential provided by some repository.
diff --git a/pkg/contexts/credentials/internal/errors.go b/api/credentials/internal/errors.go
similarity index 100%
rename from pkg/contexts/credentials/internal/errors.go
rename to api/credentials/internal/errors.go
diff --git a/pkg/contexts/credentials/internal/identity.go b/api/credentials/internal/identity.go
similarity index 100%
rename from pkg/contexts/credentials/internal/identity.go
rename to api/credentials/internal/identity.go
diff --git a/api/credentials/internal/logging.go b/api/credentials/internal/logging.go
new file mode 100644
index 000000000..d270b99e0
--- /dev/null
+++ b/api/credentials/internal/logging.go
@@ -0,0 +1,10 @@
+package internal
+
+import (
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var (
+ REALM = ocmlog.DefineSubRealm("Credentials", "credentials")
+ log = ocmlog.DynamicLogger(REALM)
+)
diff --git a/api/credentials/internal/repository.go b/api/credentials/internal/repository.go
new file mode 100644
index 000000000..337d2f4bc
--- /dev/null
+++ b/api/credentials/internal/repository.go
@@ -0,0 +1,63 @@
+package internal
+
+import (
+ "github.com/mandelsoft/goutils/set"
+
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Repository interface {
+ ExistsCredentials(name string) (bool, error)
+ LookupCredentials(name string) (Credentials, error)
+ WriteCredentials(name string, creds Credentials) (Credentials, error)
+}
+
+type Credentials interface {
+ CredentialsSource
+ ExistsProperty(name string) bool
+ GetProperty(name string) string
+ PropertyNames() set.Set[string]
+ Properties() common.Properties
+}
+
+type DirectCredentials common.Properties
+
+var _ Credentials = (*DirectCredentials)(nil)
+
+func NewCredentials(props common.Properties) DirectCredentials {
+ if props == nil {
+ props = common.Properties{}
+ } else {
+ props = props.Copy()
+ }
+ return DirectCredentials(props)
+}
+
+func (c DirectCredentials) ExistsProperty(name string) bool {
+ _, ok := c[name]
+ return ok
+}
+
+func (c DirectCredentials) GetProperty(name string) string {
+ return c[name]
+}
+
+func (c DirectCredentials) PropertyNames() set.Set[string] {
+ return common.Properties(c).Names()
+}
+
+func (c DirectCredentials) Properties() common.Properties {
+ return common.Properties(c).Copy()
+}
+
+func (c DirectCredentials) Credentials(Context, ...CredentialsSource) (Credentials, error) {
+ return c, nil
+}
+
+func (c DirectCredentials) Copy() DirectCredentials {
+ return DirectCredentials(common.Properties(c).Copy())
+}
+
+func (c DirectCredentials) String() string {
+ return common.Properties(c).String()
+}
diff --git a/api/credentials/internal/repotypes.go b/api/credentials/internal/repotypes.go
new file mode 100644
index 000000000..dafe999f3
--- /dev/null
+++ b/api/credentials/internal/repotypes.go
@@ -0,0 +1,138 @@
+package internal
+
+import (
+ "encoding/json"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/modern-go/reflect2"
+
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+ "ocm.software/ocm/api/utils/runtime/descriptivetype"
+)
+
+type RepositoryType interface {
+ descriptivetype.TypedObjectType[RepositorySpec]
+}
+
+type RepositorySpec interface {
+ runtime.VersionedTypedObject
+
+ Repository(Context, Credentials) (Repository, error)
+}
+
+type (
+ RepositorySpecDecoder = runtime.TypedObjectDecoder[RepositorySpec]
+ RepositoryTypeProvider = runtime.KnownTypesProvider[RepositorySpec, RepositoryType]
+)
+
+type RepositoryTypeScheme interface {
+ descriptivetype.TypeScheme[RepositorySpec, RepositoryType]
+}
+
+type _Scheme = descriptivetype.TypeScheme[RepositorySpec, RepositoryType]
+
+type repositoryTypeScheme struct {
+ _Scheme
+}
+
+func NewRepositoryTypeScheme(defaultDecoder RepositorySpecDecoder, base ...RepositoryTypeScheme) RepositoryTypeScheme {
+ scheme := descriptivetype.MustNewDefaultTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, &UnknownRepositorySpec{}, true, defaultDecoder, utils.Optional(base...))
+ return &repositoryTypeScheme{scheme}
+}
+
+func NewStrictRepositoryTypeScheme(base ...RepositoryTypeScheme) runtime.VersionedTypeRegistry[RepositorySpec, RepositoryType] {
+ scheme := descriptivetype.MustNewDefaultTypeScheme[RepositorySpec, RepositoryType, RepositoryTypeScheme]("Credential provider", nil, nil, false, nil, utils.Optional(base...))
+ return &repositoryTypeScheme{scheme}
+}
+
+func (t *repositoryTypeScheme) KnownTypes() runtime.KnownTypes[RepositorySpec, RepositoryType] {
+ return t._Scheme.KnownTypes()
+}
+
+// DefaultRepositoryTypeScheme contains all globally known access serializer.
+var DefaultRepositoryTypeScheme = NewRepositoryTypeScheme(nil)
+
+func RegisterRepositoryType(atype RepositoryType) {
+ DefaultRepositoryTypeScheme.Register(atype)
+}
+
+func CreateRepositorySpec(t runtime.TypedObject) (RepositorySpec, error) {
+ return DefaultRepositoryTypeScheme.Convert(t)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type UnknownRepositorySpec struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+}
+
+var (
+ _ RepositorySpec = &UnknownRepositorySpec{}
+ _ runtime.Unknown = &UnknownRepositorySpec{}
+)
+
+func (r *UnknownRepositorySpec) IsUnknown() bool {
+ return true
+}
+
+func (r *UnknownRepositorySpec) Repository(Context, Credentials) (Repository, error) {
+ return nil, errors.ErrUnknown("repository type", r.GetType())
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type GenericRepositorySpec struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+}
+
+var _ RepositorySpec = &GenericRepositorySpec{}
+
+func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error) {
+ if reflect2.IsNil(spec) {
+ return nil, nil
+ }
+ if g, ok := spec.(*GenericRepositorySpec); ok {
+ return g, nil
+ }
+ data, err := json.Marshal(spec)
+ if err != nil {
+ return nil, err
+ }
+ return newGenericRepositorySpec(data, runtime.DefaultJSONEncoding)
+}
+
+func NewGenericRepositorySpec(data []byte, unmarshaler runtime.Unmarshaler) (RepositorySpec, error) {
+ return generics.CastPointerR[RepositorySpec](newGenericRepositorySpec(data, unmarshaler))
+}
+
+func newGenericRepositorySpec(data []byte, unmarshaler runtime.Unmarshaler) (*GenericRepositorySpec, error) {
+ unstr := &runtime.UnstructuredVersionedTypedObject{}
+ if unmarshaler == nil {
+ unmarshaler = runtime.DefaultYAMLEncoding
+ }
+ err := unmarshaler.Unmarshal(data, unstr)
+ if err != nil {
+ return nil, err
+ }
+ return &GenericRepositorySpec{*unstr}, nil
+}
+
+func (s *GenericRepositorySpec) Evaluate(ctx Context) (RepositorySpec, error) {
+ raw, err := s.GetRaw()
+ if err != nil {
+ return nil, err
+ }
+ return ctx.RepositoryTypes().Decode(raw, runtime.DefaultJSONEncoding)
+}
+
+func (s *GenericRepositorySpec) Repository(ctx Context, creds Credentials) (Repository, error) {
+ spec, err := s.Evaluate(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return spec.Repository(ctx, creds)
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/pkg/contexts/credentials/internal/suite_test.go b/api/credentials/internal/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/internal/suite_test.go
rename to api/credentials/internal/suite_test.go
diff --git a/pkg/contexts/credentials/internal/utils.go b/api/credentials/internal/utils.go
similarity index 100%
rename from pkg/contexts/credentials/internal/utils.go
rename to api/credentials/internal/utils.go
diff --git a/pkg/contexts/credentials/suite_test.go b/api/credentials/suite_test.go
similarity index 100%
rename from pkg/contexts/credentials/suite_test.go
rename to api/credentials/suite_test.go
diff --git a/pkg/contexts/credentials/usage.go b/api/credentials/usage.go
similarity index 100%
rename from pkg/contexts/credentials/usage.go
rename to api/credentials/usage.go
diff --git a/api/credentials/utils.go b/api/credentials/utils.go
new file mode 100644
index 000000000..ba5295ef6
--- /dev/null
+++ b/api/credentials/utils.go
@@ -0,0 +1,139 @@
+package credentials
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/texttheater/golang-levenshtein/levenshtein"
+
+ "ocm.software/ocm/api/credentials/internal"
+ "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
+ "ocm.software/ocm/api/utils"
+)
+
+func GetProvidedConsumerId(obj interface{}, uctx ...UsageContext) ConsumerIdentity {
+ return utils.UnwrappingCall(obj, func(provider ConsumerIdentityProvider) ConsumerIdentity {
+ return provider.GetConsumerId(uctx...)
+ })
+}
+
+func GetProvidedIdentityMatcher(obj interface{}) string {
+ return utils.UnwrappingCall(obj, func(provider ConsumerIdentityProvider) string {
+ return provider.GetIdentityMatcher()
+ })
+}
+
+func CredentialsFor(ctx ContextProvider, obj interface{}, uctx ...UsageContext) (Credentials, error) {
+ id := GetProvidedConsumerId(obj, uctx...)
+ if id == nil {
+ return nil, errors.ErrNotSupported(KIND_CONSUMER)
+ }
+ return CredentialsForConsumer(ctx, id)
+}
+
+func GetRootCAs(ctx ContextProvider, creds Credentials) (*x509.CertPool, error) {
+ var rootCAs *x509.CertPool
+ var err error
+
+ if creds != nil {
+ c := creds.GetProperty(internal.ATTR_CERTIFICATE_AUTHORITY)
+ if c != "" {
+ rootCAs = x509.NewCertPool()
+ rootCAs.AppendCertsFromPEM([]byte(c))
+ }
+ }
+ if rootCAs == nil {
+ if ctx != nil {
+ rootCAs = rootcertsattr.Get(ctx.CredentialsContext()).GetRootCertPool(true)
+ } else {
+ rootCAs, err = x509.SystemCertPool()
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return rootCAs, nil
+}
+
+func GetClientCerts(ctx ContextProvider, creds Credentials) ([]tls.Certificate, error) {
+ if creds != nil {
+ cert := creds.GetProperty(internal.ATTR_CERTIFICATE)
+ priv := creds.GetProperty(internal.ATTR_PRIVATE_KEY)
+ if cert == "" && priv == "" {
+ return nil, nil
+ }
+ if cert == "" || priv == "" {
+ return nil, errors.New("both, private key and certificate are required for tls client authentication")
+ }
+ if cert != "" && priv != "" {
+ tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(priv))
+ if err != nil {
+ return nil, err
+ }
+ return []tls.Certificate{tlsCert}, nil
+ }
+ }
+ return nil, nil
+}
+
+func GuessConsumerType(ctxp ContextProvider, spec string) string {
+ matchers := ctxp.CredentialsContext().ConsumerIdentityMatchers()
+ lspec := strings.ToLower(spec)
+
+ if matchers.Get(spec) == nil {
+ fix := ""
+ for _, i := range matchers.List() {
+ idx := strings.Index(i.Type, ".")
+ if idx > 0 && i.Type[:idx] == spec {
+ fix = i.Type
+ break
+ }
+ }
+ if fix == "" {
+ for _, i := range matchers.List() {
+ if strings.ToLower(i.Type) == lspec {
+ fix = i.Type
+ break
+ }
+ }
+ }
+ if fix == "" {
+ for _, i := range matchers.List() {
+ idx := strings.Index(i.Type, ".")
+ if idx > 0 && strings.ToLower(i.Type[:idx]) == lspec {
+ fix = i.Type
+ break
+ }
+ }
+ }
+ if fix == "" {
+ min := -1
+ for _, i := range matchers.List() {
+ idx := strings.Index(i.Type, ".")
+ if idx > 0 {
+ d := levenshtein.DistanceForStrings([]rune(lspec), []rune(strings.ToLower(i.Type[:idx])), levenshtein.DefaultOptions)
+ if d < 5 && fix == "" || min > d {
+ fix = i.Type
+ min = d
+ }
+ }
+ }
+ }
+ if fix == "" {
+ min := -1
+ for _, i := range matchers.List() {
+ d := levenshtein.DistanceForStrings([]rune(lspec), []rune(strings.ToLower(i.Type)), levenshtein.DefaultOptions)
+ if d < 5 && fix == "" || min > d {
+ fix = i.Type
+ min = d
+ }
+ }
+ }
+ if fix != "" {
+ return fix
+ }
+ }
+ return spec
+}
diff --git a/api/datacontext/action/api/action_test.go b/api/datacontext/action/api/action_test.go
new file mode 100644
index 000000000..a3ba0d119
--- /dev/null
+++ b/api/datacontext/action/api/action_test.go
@@ -0,0 +1,108 @@
+package api_test
+
+import (
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/datacontext/action/api"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const NAME = "testAction"
+
+const CONSUMER_TYPE = "TestAction"
+
+const ID_HOSTNAME = hostpath.ID_HOSTNAME
+
+func RegisterAction(registry api.ActionTypeRegistry) {
+ registry.RegisterAction(NAME, "test action", "nothing special", []string{ID_HOSTNAME})
+
+ registry.RegisterActionType(api.NewActionType[*ActionSpec, *ActionResult](NAME, "v1"))
+ registry.RegisterActionType(api.NewActionTypeByConverter[*ActionSpec, *ActionSpecV2, *ActionResult, *ActionResultV2](NAME, "v2", convertSpecV2{}, convertResultV2{}))
+}
+
+func NewActionSpec(field string) api.ActionSpec {
+ return &ActionSpec{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v1")),
+ Field: field,
+ }
+}
+
+func NewActionResult(msg string) api.ActionResult {
+ return &ActionResult{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v1")),
+ Message: msg,
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// internal version
+
+type ActionSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Field string `json:"field"`
+}
+
+func (a *ActionSpec) Selector() api.Selector {
+ return api.Selector(a.Field)
+}
+
+func (a *ActionSpec) GetConsumerAttributes() common.Properties {
+ return common.Properties(credentials.NewConsumerIdentity(CONSUMER_TYPE,
+ ID_HOSTNAME, a.Field,
+ ))
+}
+
+type ActionResult struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Message string `json:"message"`
+}
+
+func (r ActionResult) GetMessage() string {
+ return r.Message
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// external version
+
+type ActionSpecV2 struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Data string `json:"data"`
+}
+
+type ActionResultV2 struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Data string `json:"data"`
+}
+
+type convertSpecV2 struct{}
+
+func (c convertSpecV2) ConvertFrom(in *ActionSpec) (*ActionSpecV2, error) {
+ return &ActionSpecV2{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v2")),
+ Data: in.Field,
+ }, nil
+}
+
+func (c convertSpecV2) ConvertTo(in *ActionSpecV2) (*ActionSpec, error) {
+ return &ActionSpec{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v2")),
+ Field: in.Data,
+ }, nil
+}
+
+type convertResultV2 struct{}
+
+func (c convertResultV2) ConvertFrom(in *ActionResult) (*ActionResultV2, error) {
+ return &ActionResultV2{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v2")),
+ Data: in.Message,
+ }, nil
+}
+
+func (c convertResultV2) ConvertTo(in *ActionResultV2) (*ActionResult, error) {
+ return &ActionResult{
+ ObjectVersionedType: runtime.NewVersionedObjectType(runtime.TypeName(NAME, "v2")),
+ Message: in.Data,
+ }, nil
+}
diff --git a/api/datacontext/action/api/interface.go b/api/datacontext/action/api/interface.go
new file mode 100644
index 000000000..545ae7985
--- /dev/null
+++ b/api/datacontext/action/api/interface.go
@@ -0,0 +1,84 @@
+package api
+
+import (
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type Action interface {
+ Name() string
+ Description() string
+ Usage() string
+ ConsumerAttributes() []string
+ GetVersion(string) ActionType
+ SupportedVersions() []string
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Action Specification
+
+type Selector string
+
+func (s Selector) ApplyActionHandlerOptionTo(opts *Options) {
+ opts.Selectors = append(opts.Selectors, s)
+}
+
+type ActionSpec interface {
+ runtime.VersionedTypedObject
+ SetVersion(string)
+ Selector() Selector
+ GetConsumerAttributes() common.Properties
+}
+
+type ActionSpecType runtime.VersionedTypedObjectType[ActionSpec]
+
+////////////////////////////////////////////////////////////////////////////////
+// Action Result
+
+type ActionResult interface {
+ runtime.VersionedTypedObject
+ SetVersion(string)
+ SetType(string)
+ GetMessage() string
+}
+
+// CommonResult is the minimal action result.
+type CommonResult struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Message string `json:"message,omitempty"`
+}
+
+func (r *CommonResult) GetMessage() string {
+ return r.Message
+}
+
+func (r *CommonResult) SetType(typ string) {
+ r.Type = typ
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Action Type
+
+type ActionResultType runtime.VersionedTypedObjectType[ActionResult]
+
+type ActionType interface {
+ runtime.VersionedTypedObject
+ SpecificationType() ActionSpecType
+ ResultType() ActionResultType
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Options Type
+
+type Option interface {
+ ApplyActionHandlerOptionTo(*Options)
+}
+
+type Options struct {
+ Action string
+ Selectors []Selector
+ Priority int
+ Versions []string
+}
+
+var _ Option = (*Options)(nil)
diff --git a/pkg/contexts/datacontext/action/api/options.go b/api/datacontext/action/api/options.go
similarity index 100%
rename from pkg/contexts/datacontext/action/api/options.go
rename to api/datacontext/action/api/options.go
diff --git a/api/datacontext/action/api/registry.go b/api/datacontext/action/api/registry.go
new file mode 100644
index 000000000..bf2ceee6d
--- /dev/null
+++ b/api/datacontext/action/api/registry.go
@@ -0,0 +1,235 @@
+package api
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ KIND_ACTION = "action"
+ KIND_ACTIONTYPE = "action type"
+)
+
+type ActionTypeRegistry interface {
+ RegisterAction(name string, description string, usage string, attrs []string) error
+ RegisterActionType(typ ActionType) error
+
+ DecodeActionSpec(data []byte, unmarshaler runtime.Unmarshaler) (ActionSpec, error)
+ EncodeActionSpec(spec ActionSpec, marshaler runtime.Marshaler) ([]byte, error)
+
+ DecodeActionResult(data []byte, unmarshaler runtime.Unmarshaler) (ActionResult, error)
+ EncodeActionResult(spec ActionResult, marshaler runtime.Marshaler) ([]byte, error)
+
+ GetAction(name string) Action
+ SupportedActionVersions(name string) []string
+
+ Copy() ActionTypeRegistry
+}
+
+type action struct {
+ lock sync.Mutex
+ name string
+ shortdesc string
+ usage string
+ attributes []string
+ types map[string]ActionType
+}
+
+var _ Action = (*action)(nil)
+
+func (a *action) Name() string {
+ return a.name
+}
+
+func (a *action) Description() string {
+ return a.shortdesc
+}
+
+func (a *action) Usage() string {
+ return a.usage
+}
+
+func (a *action) ConsumerAttributes() []string {
+ return a.attributes
+}
+
+func (a *action) GetVersion(v string) ActionType {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+ return a.types[v]
+}
+
+func (a *action) SupportedVersions() []string {
+ return utils.StringMapKeys(a.types)
+}
+
+type actionRegistry struct {
+ lock sync.Mutex
+ actions map[string]*action
+ actionspecs runtime.TypeScheme[ActionSpec, ActionSpecType]
+ resultspecs runtime.TypeScheme[ActionResult, ActionResultType]
+}
+
+func NewActionTypeRegistry() ActionTypeRegistry {
+ return &actionRegistry{
+ actions: map[string]*action{},
+ actionspecs: runtime.NewTypeScheme[ActionSpec, ActionSpecType](),
+ resultspecs: runtime.NewTypeScheme[ActionResult, ActionResultType](),
+ }
+}
+
+func (r *actionRegistry) Copy() ActionTypeRegistry {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ actions := map[string]*action{}
+
+ for k, v := range r.actions {
+ v.lock.Lock()
+ a := action{
+ name: v.name,
+ shortdesc: v.shortdesc,
+ usage: v.usage,
+ attributes: v.attributes,
+ }
+ a.types = map[string]ActionType{}
+ for _, t := range v.types {
+ a.types[t.GetType()] = t
+ }
+ v.lock.Unlock()
+ actions[k] = &a
+ }
+ actionspecs := runtime.NewTypeScheme[ActionSpec, ActionSpecType]()
+ actionspecs.AddKnownTypes(r.actionspecs)
+ resultspecs := runtime.NewTypeScheme[ActionResult, ActionResultType]()
+ resultspecs.AddKnownTypes(r.resultspecs)
+ return &actionRegistry{
+ actions: actions,
+ actionspecs: actionspecs,
+ resultspecs: resultspecs,
+ }
+}
+
+func (r *actionRegistry) RegisterAction(name string, description string, usage string, attrs []string) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ ai := r.actions[name]
+ if ai != nil {
+ return errors.ErrAlreadyExists(KIND_ACTION, name)
+ }
+
+ ai = &action{
+ name: name,
+ shortdesc: description,
+ usage: usage,
+ attributes: slices.Clone(attrs),
+ types: map[string]ActionType{},
+ }
+ r.actions[name] = ai
+ return nil
+}
+
+func (r *actionRegistry) RegisterActionType(typ ActionType) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ k := typ.GetKind()
+
+ ai := r.actions[k]
+ if ai == nil {
+ return errors.ErrNotFound(KIND_ACTION, k)
+ }
+
+ if typ.SpecificationType().GetType() != typ.ResultType().GetType() {
+ return errors.ErrInvalidWrap(fmt.Errorf("version mismatch: request[%s]!=result[%s]", typ.SpecificationType().GetType(), typ.ResultType().GetType()), KIND_ACTIONTYPE, k)
+ }
+ if typ.SpecificationType().GetKind() != k {
+ return errors.ErrInvalidWrap(fmt.Errorf("kind mismatch in types: %s", typ.SpecificationType().GetType()), KIND_ACTIONTYPE, k)
+ }
+ ai.types[typ.GetVersion()] = typ
+ ai.lock.Lock()
+ defer ai.lock.Unlock()
+ r.actionspecs.Register(typ.SpecificationType())
+ r.resultspecs.Register(typ.ResultType())
+ return nil
+}
+
+func (r *actionRegistry) GetAction(name string) Action {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ return r.actions[name]
+}
+
+func (r *actionRegistry) DecodeActionSpec(data []byte, unmarshaler runtime.Unmarshaler) (ActionSpec, error) {
+ return r.actionspecs.Decode(data, unmarshaler)
+}
+
+func (r *actionRegistry) DecodeActionResult(data []byte, unmarshaler runtime.Unmarshaler) (ActionResult, error) {
+ return r.resultspecs.Decode(data, unmarshaler)
+}
+
+func (r *actionRegistry) EncodeActionSpec(spec ActionSpec, marshaler runtime.Marshaler) ([]byte, error) {
+ return r.actionspecs.Encode(spec, marshaler)
+}
+
+func (r *actionRegistry) EncodeActionResult(spec ActionResult, marshaler runtime.Marshaler) ([]byte, error) {
+ return r.resultspecs.Encode(spec, marshaler)
+}
+
+func (r *actionRegistry) SupportedActionVersions(name string) []string {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ a := r.actions[name]
+ if a == nil {
+ return nil
+ }
+ return a.SupportedVersions()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+var registry = NewActionTypeRegistry()
+
+func DefaultRegistry() ActionTypeRegistry {
+ return registry
+}
+
+func RegisterAction(name string, description string, usage string, attrs []string) error {
+ return registry.RegisterAction(name, description, usage, attrs)
+}
+
+func RegisterType(typ ActionType) error {
+ return registry.RegisterActionType(typ)
+}
+
+func GetAction(name string) Action {
+ return registry.GetAction(name)
+}
+
+func DecodeActionSpec(data []byte, unmarshaler runtime.Unmarshaler) (ActionSpec, error) {
+ return registry.DecodeActionSpec(data, unmarshaler)
+}
+
+func EncodeActionSpec(spec ActionSpec, marshaler runtime.Marshaler) ([]byte, error) {
+ return registry.EncodeActionSpec(spec, marshaler)
+}
+
+func DecodeActionResult(data []byte, unmarshaler runtime.Unmarshaler) (ActionResult, error) {
+ return registry.DecodeActionResult(data, unmarshaler)
+}
+
+func EncodeActionResult(spec ActionResult, marshaler runtime.Marshaler) ([]byte, error) {
+ return registry.EncodeActionResult(spec, marshaler)
+}
+
+func SupportedActionVersions(name string) []string {
+ return registry.SupportedActionVersions(name)
+}
diff --git a/api/datacontext/action/api/registry_test.go b/api/datacontext/action/api/registry_test.go
new file mode 100644
index 000000000..e9c694670
--- /dev/null
+++ b/api/datacontext/action/api/registry_test.go
@@ -0,0 +1,100 @@
+package api_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/action/api"
+ "ocm.software/ocm/api/datacontext/action/handlers"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type Handler struct {
+ spec api.ActionSpec
+ creds common.Properties
+}
+
+func (h *Handler) Handle(spec api.ActionSpec, creds common.Properties) (api.ActionResult, error) {
+ h.spec = spec
+ h.creds = creds
+ r := NewActionResult(spec.(*ActionSpec).Field)
+ r.SetVersion("v2")
+ return r, nil
+}
+
+var _ handlers.ActionHandler = &Handler{}
+
+var _ = Describe("action registry", func() {
+ var registry api.ActionTypeRegistry
+
+ BeforeEach(func() {
+ registry = api.NewActionTypeRegistry()
+ RegisterAction(registry)
+ })
+
+ Context("plain", func() {
+ It("registers", func() {
+ Expect(registry.SupportedActionVersions(NAME)).To(Equal([]string{"v1", "v2"}))
+ })
+
+ It("encoding spec v1", func() {
+ spec := NewActionSpec("acme.com")
+ spec.SetVersion("v1")
+ data := Must(registry.EncodeActionSpec(spec, runtime.DefaultJSONEncoding))
+ Expect(string(data)).To(Equal(`{"type":"testAction/v1","field":"acme.com"}`))
+ d := Must(registry.DecodeActionSpec(data, runtime.DefaultJSONEncoding))
+ Expect(d).To(Equal(spec))
+ })
+ It("encoding spec v2", func() {
+ spec := NewActionSpec("acme.com")
+ spec.SetVersion("v2")
+ data := Must(registry.EncodeActionSpec(spec, runtime.DefaultJSONEncoding))
+ Expect(string(data)).To(Equal(`{"type":"testAction/v2","data":"acme.com"}`))
+ d := Must(registry.DecodeActionSpec(data, runtime.DefaultJSONEncoding))
+ Expect(d).To(Equal(spec))
+ })
+
+ It("encoding result v1", func() {
+ spec := NewActionResult("successful")
+ spec.SetVersion("v1")
+ data := Must(registry.EncodeActionResult(spec, runtime.DefaultJSONEncoding))
+ Expect(string(data)).To(Equal(`{"type":"testAction/v1","message":"successful"}`))
+ d := Must(registry.DecodeActionResult(data, runtime.DefaultJSONEncoding))
+ Expect(d).To(Equal(spec))
+ })
+ It("encoding result v2", func() {
+ spec := NewActionResult("successful")
+ spec.SetVersion("v2")
+ data := Must(registry.EncodeActionResult(spec, runtime.DefaultJSONEncoding))
+ Expect(string(data)).To(Equal(`{"type":"testAction/v2","data":"successful"}`))
+ d := Must(registry.DecodeActionResult(data, runtime.DefaultJSONEncoding))
+ Expect(d).To(Equal(spec))
+ })
+ })
+
+ Context("data context", func() {
+ var ctx datacontext.Context
+ var handler *Handler
+
+ BeforeEach(func() {
+ handler = &Handler{}
+ ctx = datacontext.NewWithActions(nil, handlers.NewRegistry(registry))
+ Expect(ctx.GetActions().GetActionTypes()).To(BeIdenticalTo(registry))
+ MustBeSuccessful(ctx.GetActions().Register(handler, handlers.ForAction(NAME), handlers.WithVersions("v2"), handlers.ForSelectors(".*\\.com")))
+ })
+
+ It("", func() {
+ spec := NewActionSpec("acme.com")
+ creds := common.Properties{"alice": "bob"}
+ r := Must(ctx.GetActions().Execute(spec, creds))
+ Expect(handler.spec).To(Equal(spec))
+ Expect(handler.creds).To(Equal(creds))
+ rs := NewActionResult("acme.com")
+ rs.SetVersion("v2")
+ Expect(r).To(Equal(rs))
+ })
+ })
+})
diff --git a/pkg/contexts/datacontext/action/api/suite_test.go b/api/datacontext/action/api/suite_test.go
similarity index 100%
rename from pkg/contexts/datacontext/action/api/suite_test.go
rename to api/datacontext/action/api/suite_test.go
diff --git a/api/datacontext/action/api/utils.go b/api/datacontext/action/api/utils.go
new file mode 100644
index 000000000..52535f92f
--- /dev/null
+++ b/api/datacontext/action/api/utils.go
@@ -0,0 +1,38 @@
+package api
+
+import (
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type _Object = runtime.ObjectVersionedTypedObject
+
+type actionType struct {
+ _Object
+ spectype ActionSpecType
+ restype ActionResultType
+}
+
+var _ ActionType = (*actionType)(nil)
+
+func NewActionType[IS ActionSpec, IR ActionResult](kind, version string) ActionType {
+ return NewActionTypeByConverter[IS, IS, IR, IR](kind, version, runtime.IdentityConverter[IS]{}, runtime.IdentityConverter[IR]{})
+}
+
+func NewActionTypeByConverter[IS ActionSpec, VS runtime.TypedObject, IR ActionResult, VR runtime.TypedObject](kind, version string, specconv runtime.Converter[IS, VS], resconv runtime.Converter[IR, VR]) ActionType {
+ name := runtime.TypeName(kind, version)
+ st := runtime.NewVersionedTypedObjectTypeByConverter[ActionSpec, IS, VS](name, specconv)
+ rt := runtime.NewVersionedTypedObjectTypeByConverter[ActionResult, IR, VR](name, resconv)
+ return &actionType{
+ _Object: runtime.NewVersionedTypedObject(kind, version),
+ spectype: st,
+ restype: rt,
+ }
+}
+
+func (a *actionType) SpecificationType() ActionSpecType {
+ return a.spectype
+}
+
+func (a *actionType) ResultType() ActionResultType {
+ return a.restype
+}
diff --git a/api/datacontext/action/handlers/options.go b/api/datacontext/action/handlers/options.go
new file mode 100644
index 000000000..9928a0d0c
--- /dev/null
+++ b/api/datacontext/action/handlers/options.go
@@ -0,0 +1,72 @@
+package handlers
+
+import (
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/datacontext/action/api"
+)
+
+type (
+ Option = api.Option
+ Options = api.Options
+)
+
+func NewOptions(opts ...Option) *Options {
+ return api.NewOptions(opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type kind struct {
+ action string
+}
+
+func ForAction(a string) Option {
+ return kind{a}
+}
+
+func (o kind) ApplyActionHandlerOptionTo(opts *Options) {
+ opts.Action = o.action
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type prio struct {
+ prio int
+}
+
+func WithPrio(p int) Option {
+ return prio{p}
+}
+
+func (o prio) ApplyActionHandlerOptionTo(opts *Options) {
+ opts.Priority = o.prio
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type selectors struct {
+ selectors []api.Selector
+}
+
+func ForSelectors(s ...api.Selector) Option {
+ return selectors{s}
+}
+
+func (o selectors) ApplyActionHandlerOptionTo(opts *Options) {
+ opts.Selectors = append(opts.Selectors, o.selectors...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type versions struct {
+ versions []string
+}
+
+func WithVersions(vers ...string) Option {
+ return versions{slices.Clone(vers)}
+}
+
+func (o versions) ApplyActionHandlerOptionTo(opts *Options) {
+ opts.Versions = append(opts.Versions, o.versions...)
+}
diff --git a/api/datacontext/action/handlers/registry.go b/api/datacontext/action/handlers/registry.go
new file mode 100644
index 000000000..7751f1f19
--- /dev/null
+++ b/api/datacontext/action/handlers/registry.go
@@ -0,0 +1,233 @@
+package handlers
+
+import (
+ "fmt"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/datacontext/action/api"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/registrations"
+ "ocm.software/ocm/api/utils/semverutils"
+)
+
+var defaultHandlers = NewRegistry(api.DefaultRegistry())
+
+func DefaultRegistry() Registry {
+ return defaultHandlers
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type ActionsProvider interface {
+ GetActions() Registry
+}
+
+type ActionHandler interface {
+ Handle(api.ActionSpec, common.Properties) (api.ActionResult, error)
+}
+
+type ActionHandlerMatch struct {
+ Handler ActionHandler
+ Version string
+ Priority int
+}
+
+type (
+ Target = ActionsProvider
+ HandlerConfig = registrations.HandlerConfig
+ HandlerRegistrationHandler = registrations.HandlerRegistrationHandler[Target, Option]
+ HandlerRegistrationRegistry = registrations.HandlerRegistrationRegistry[Target, Option]
+)
+
+func NewHandlerRegistrationRegistry(base ...HandlerRegistrationRegistry) HandlerRegistrationRegistry {
+ return registrations.NewHandlerRegistrationRegistry[Target, Option](base...)
+}
+
+type Registry interface {
+ registrations.HandlerRegistrationRegistry[Target, Option]
+
+ GetActionTypes() api.ActionTypeRegistry
+
+ Register(h ActionHandler, opts ...Option) error
+ Execute(spec api.ActionSpec, creds common.Properties) (api.ActionResult, error)
+ Get(spec api.ActionSpec, possible ...string) []ActionHandlerMatch
+ AddTo(t Registry)
+}
+
+type registration struct {
+ handler ActionHandler
+ versions []string
+ priority int
+}
+
+var _ Option = (*registration)(nil)
+
+func (r *registration) ApplyActionHandlerOptionTo(opts *api.Options) {
+ opts.Priority = r.priority
+}
+
+type registry struct {
+ registrations.HandlerRegistrationRegistry[Target, Option]
+ types api.ActionTypeRegistry
+
+ lock sync.Mutex
+ base Registry
+ registrations map[string]map[api.Selector]*registration
+}
+
+var _ Registry = (*registry)(nil)
+
+func NewRegistry(types api.ActionTypeRegistry, base ...Registry) Registry {
+ b := utils.Optional(base...)
+ if types == nil {
+ if b == nil {
+ types = api.DefaultRegistry()
+ } else {
+ types = b.GetActionTypes()
+ }
+ }
+ r := ®istry{
+ base: b,
+ types: types,
+ registrations: map[string]map[api.Selector]*registration{},
+ HandlerRegistrationRegistry: NewHandlerRegistrationRegistry(b),
+ }
+ return r
+}
+
+func (r *registry) GetActionTypes() api.ActionTypeRegistry {
+ return r.types
+}
+
+func (r *registry) AddTo(t Registry) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ if r.base != nil {
+ r.base.AddTo(t)
+ }
+ for k, sel := range r.registrations {
+ for s, reg := range sel {
+ t.Register(reg.handler, ForAction(k), WithVersions(reg.versions...), s, reg)
+ }
+ }
+}
+
+func (r *registry) Register(h ActionHandler, olist ...Option) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ opts := NewOptions(olist...)
+ if opts.Action == "" {
+ return fmt.Errorf("action kind required for action handler registration")
+ }
+
+ kinds := r.registrations[opts.Action]
+ if kinds == nil {
+ kinds = map[api.Selector]*registration{}
+ r.registrations[opts.Action] = kinds
+ }
+
+ versions := opts.Versions
+ if versions == nil {
+ versions = r.types.SupportedActionVersions(opts.Action)
+ }
+ versions = slices.Clone(versions)
+ if err := semverutils.SortVersions(versions); err != nil {
+ return errors.Wrapf(err, "invalid version set")
+ }
+ reg := ®istration{
+ handler: h,
+ versions: versions,
+ priority: general.Conditional(opts.Priority >= 0, opts.Priority, 10),
+ }
+
+ for _, s := range opts.Selectors {
+ kinds[s] = reg
+ }
+ return nil
+}
+
+func (r *registry) Execute(spec api.ActionSpec, creds common.Properties) (api.ActionResult, error) {
+ result := r.Get(spec)
+ sort.SliceStable(result, func(a, b int) bool {
+ return result[a].Priority < result[b].Priority
+ })
+ if len(result) > 0 {
+ spec.SetVersion(result[0].Version)
+ return result[0].Handler.Handle(spec, creds)
+ }
+ return nil, nil
+}
+
+func (r *registry) Get(spec api.ActionSpec, possible ...string) []ActionHandlerMatch {
+ if len(possible) == 0 {
+ possible = r.GetActionTypes().SupportedActionVersions(spec.GetKind())
+ }
+
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ var result []ActionHandlerMatch
+
+ if kinds := r.registrations[spec.GetKind()]; kinds != nil {
+ // first, check direct selctor match
+ if reg := kinds[spec.Selector()]; reg != nil {
+ if len(reg.versions) != 0 {
+ if v := MatchVersion(r.types.SupportedActionVersions(spec.GetKind()), reg.versions); v != "" {
+ result = append(result, ActionHandlerMatch{Handler: reg.handler, Version: v, Priority: reg.priority})
+ }
+ }
+ } else {
+ // second, try registrations as regexp matcher
+ for sel, reg := range kinds {
+ s := string(sel)
+ e, err := regexp.Compile(s)
+ if err == nil {
+ t := strings.Trim(s, "^$")
+ if t == s {
+ e, err = regexp.Compile("^" + s + "$")
+ }
+ }
+ if err == nil {
+ if e.MatchString(string(spec.Selector())) {
+ if v := MatchVersion(r.types.SupportedActionVersions(spec.GetKind()), reg.versions); v != "" {
+ result = append(result, ActionHandlerMatch{Handler: reg.handler, Version: v, Priority: reg.priority})
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if r.base != nil {
+ result = append(result, r.base.Get(spec, possible...)...)
+ }
+ return result
+}
+
+func MatchVersion(possible []string, avail []string) string {
+ p := slices.Clone(possible)
+ a := slices.Clone(avail)
+
+ semverutils.SortVersions(p)
+ semverutils.SortVersions(a)
+ f := ""
+ for _, v := range p {
+ for _, c := range a {
+ if v == c {
+ f = c
+ break
+ }
+ }
+ }
+ return f
+}
diff --git a/api/datacontext/action/type.go b/api/datacontext/action/type.go
new file mode 100644
index 000000000..fb74805bd
--- /dev/null
+++ b/api/datacontext/action/type.go
@@ -0,0 +1,20 @@
+package action
+
+import (
+ "ocm.software/ocm/api/datacontext/action/api"
+)
+
+const KIND_ACTION = api.KIND_ACTION
+
+type (
+ Selector = api.Selector
+ Action = api.Action
+ ActionSpec = api.ActionSpec
+ ActionResult = api.ActionResult
+ ActionType = api.ActionType
+ ActionTypeRegistry = api.ActionTypeRegistry
+)
+
+func DefaultRegistry() ActionTypeRegistry {
+ return api.DefaultRegistry()
+}
diff --git a/pkg/contexts/datacontext/attrs.go b/api/datacontext/attrs.go
similarity index 97%
rename from pkg/contexts/datacontext/attrs.go
rename to api/datacontext/attrs.go
index a68503c64..80cc13e49 100644
--- a/pkg/contexts/datacontext/attrs.go
+++ b/api/datacontext/attrs.go
@@ -6,8 +6,8 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/runtime"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
)
type AttributeType interface {
diff --git a/api/datacontext/attrs/clicfgattr/attr.go b/api/datacontext/attrs/clicfgattr/attr.go
new file mode 100644
index 000000000..41deb2cc4
--- /dev/null
+++ b/api/datacontext/attrs/clicfgattr/attr.go
@@ -0,0 +1,70 @@
+package clicfgattr
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "sigs.k8s.io/yaml"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "ocm.software/cliconfig"
+ ATTR_SHORT = "cliconfig"
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*cliconfigr* Configuration Object passed to command line pluging.
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ switch c := v.(type) {
+ case config.Config:
+ return json.Marshal(v)
+ case []byte:
+ if _, err := a.Decode(c, nil); err != nil {
+ return nil, err
+ }
+ return c, nil
+ default:
+ return nil, fmt.Errorf("config object required")
+ }
+}
+
+func (a AttributeType) Decode(data []byte, _ runtime.Unmarshaler) (interface{}, error) {
+ var c config.GenericConfig
+ err := yaml.Unmarshal(data, &c)
+ if err != nil {
+ return nil, err
+ }
+ return &c, nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx datacontext.Context) config.Config {
+ v := ctx.GetAttributes().GetAttribute(ATTR_KEY)
+ if v == nil {
+ return nil
+ }
+ return v.(config.Config)
+}
+
+func Set(ctx datacontext.Context, c config.Config) {
+ ctx.GetAttributes().SetAttribute(ATTR_KEY, c)
+}
diff --git a/api/datacontext/attrs/init.go b/api/datacontext/attrs/init.go
new file mode 100644
index 000000000..9b87f58cb
--- /dev/null
+++ b/api/datacontext/attrs/init.go
@@ -0,0 +1,8 @@
+package attrs
+
+import (
+ _ "ocm.software/ocm/api/datacontext/attrs/logforward"
+ _ "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
+ _ "ocm.software/ocm/api/datacontext/attrs/tmpcache"
+ _ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+)
diff --git a/api/datacontext/attrs/logforward/attr.go b/api/datacontext/attrs/logforward/attr.go
new file mode 100644
index 000000000..1fb339645
--- /dev/null
+++ b/api/datacontext/attrs/logforward/attr.go
@@ -0,0 +1,66 @@
+package logforward
+
+import (
+ "encoding/json"
+ "fmt"
+
+ logcfg "github.com/mandelsoft/logging/config"
+ "sigs.k8s.io/yaml"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "github.com/mandelsoft/logforward"
+ ATTR_SHORT = "logfwd"
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*logconfig* Logging config structure used for config forwarding
+This attribute is used to specify a logging configuration intended
+to be forwarded to other tools.
+(For example: TOI passes this config to the executor)
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ if _, ok := v.(*logcfg.Config); !ok {
+ return nil, fmt.Errorf("logging config required")
+ }
+ return json.Marshal(v)
+}
+
+func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) {
+ var c logcfg.Config
+ err := yaml.Unmarshal(data, &c)
+ if err != nil {
+ return nil, err
+ }
+ return &c, nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx datacontext.Context) *logcfg.Config {
+ v := ctx.GetAttributes().GetAttribute(ATTR_KEY)
+ if v == nil {
+ return nil
+ }
+ return v.(*logcfg.Config)
+}
+
+func Set(ctx datacontext.Context, c *logcfg.Config) {
+ ctx.GetAttributes().SetAttribute(ATTR_KEY, c)
+}
diff --git a/api/datacontext/attrs/rootcertsattr/attr.go b/api/datacontext/attrs/rootcertsattr/attr.go
new file mode 100644
index 000000000..c5105d717
--- /dev/null
+++ b/api/datacontext/attrs/rootcertsattr/attr.go
@@ -0,0 +1,148 @@
+package rootcertsattr
+
+import (
+ "crypto/x509"
+ "encoding/json"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/tech/signing/signutils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "github.com/mandelsoft/ocm/rootcerts"
+ ATTR_SHORT = "rootcerts"
+)
+
+type (
+ Context = datacontext.AttributesContext
+ ContextProvider = datacontext.ContextProvider
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*JSON*
+General root certificate settings given as JSON document with the following
+format:
+
++{ + "rootCertificates"": [ + { + "data": ""<base64>" + }, + { + "path": ""<file path>" + } + ], ++ +One of following data fields are possible: +-
data
: base64 encoded binary data
+- stringdata
: plain text data
+- path
: a file path to read the data from
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ attr, ok := v.(*Attribute)
+ if !ok {
+ return nil, errors.ErrInvalid("certificate attribute")
+ }
+ cfg := New()
+
+ attr.lock.Lock()
+ defer attr.lock.Unlock()
+
+ for _, c := range attr.rootCertificates {
+ data := signutils.CertificateToPem(c)
+ cfg.AddRootCertificateData(data)
+ }
+
+ return json.Marshal(cfg)
+}
+
+func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) {
+ var value Config
+ err := unmarshaller.Unmarshal(data, &value)
+ if err != nil {
+ return nil, err
+ }
+
+ attr := &Attribute{}
+ err = value.ApplyToAttribute(attr)
+ if err != nil {
+ return nil, err
+ }
+ return attr, nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type Attribute struct {
+ lock sync.Mutex
+ rootCertificates []*x509.Certificate
+}
+
+func (a *Attribute) RegisterRootCertificates(cert signutils.GenericCertificateChain) error {
+ certs, err := signutils.GetCertificateChain(cert, false)
+ if err != nil {
+ return err
+ }
+
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ a.rootCertificates = append(a.rootCertificates, certs...)
+ return nil
+}
+
+func (a *Attribute) HasRootCertificates() bool {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+ return len(a.rootCertificates) > 0
+}
+
+func (a *Attribute) GetRootCertPool(system bool) *x509.CertPool {
+ var pool *x509.CertPool
+
+ if system {
+ pool, _ = x509.SystemCertPool()
+ }
+ if pool == nil {
+ pool = x509.NewCertPool()
+ }
+
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ for _, c := range a.rootCertificates {
+ pool.AddCert(c)
+ }
+ return pool
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx ContextProvider) *Attribute {
+ return ctx.AttributesContext().GetAttributes().GetOrCreateAttribute(ATTR_KEY, func(ctx datacontext.Context) interface{} {
+ return &Attribute{}
+ }).(*Attribute)
+}
+
+func Set(ctx ContextProvider, attribute *Attribute) error {
+ return ctx.AttributesContext().GetAttributes().SetAttribute(ATTR_KEY, attribute)
+}
diff --git a/api/datacontext/attrs/rootcertsattr/attr_test.go b/api/datacontext/attrs/rootcertsattr/attr_test.go
new file mode 100644
index 000000000..a9f0081e9
--- /dev/null
+++ b/api/datacontext/attrs/rootcertsattr/attr_test.go
@@ -0,0 +1,66 @@
+package rootcertsattr_test
+
+import (
+ "encoding/json"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/datacontext"
+ me "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
+)
+
+const NAME = "test"
+
+var certdata = []byte(`
+-----BEGIN CERTIFICATE-----
+MIIDBDCCAeygAwIBAgIQF+kRr0G+faDEAH5Y4P1J7DANBgkqhkiG9w0BAQsFADAc
+MQwwCgYDVQQKEwNPQ00xDDAKBgNVBAMTA29jbTAeFw0yMzEyMjkxMDIyMzdaFw0y
+NDEyMjgxMDIyMzdaMBwxDDAKBgNVBAoTA09DTTEMMAoGA1UEAxMDb2NtMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpTQIQFNy23ygef3pshdeNjT7TME
+kPEuqrqcF3KIX1cX16pHMQeU+VzXAFRj3xCy3LAM8ZzLsdHSwZDsIsGdg0nAbGjz
++USez/9TGC58ktr/84Kh0gHDE28YSVhsnNSrBJcWaBlYZz4Iy89O2Xc4jbK34Cwg
+Si0ES+Ru1lxLD6FSLYLe43wCIjWRJRrMFcua6nI0P4MCpcKmTkXG2/xz80QSobI3
+z/isqOT54FKHW8DZZVlQMOxh+loeLksfEq7EYVkQoUWEV6xyR24TEpMGfxERgDre
+l7lmx8nIFzRMXkot+P19XWfUBgqctVEiDF4DlRE+SvCZsNCrg7nQuC2AZQIDAQAB
+o0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+1iQqrWM/bCXMk+5c1bulfI5zlKcwDQYJKoZIhvcNAQELBQADggEBAAQO6lw6ePuX
+E+NyhDYCulueMWHC7GRUKa1KpouFT2yM0BSQnP04VakTlwVO3w4w2KucSVVomHR3
+hTY9Ypx7iGLaqdXHmUZvx3uaTM5IXQKMMWL1LJsxAvuzucehgDlOnFBD91tKsr5o
+VRvRU5ya0igBCnnGpFu7NuH3C9pgF01lrQ3EhUHuNeazxleaE3/uQWmAXfxFB4ci
+gHMKSEk3HuYA1raDJFv4ihwO5pXHvlDhcW0C1oMG9lOCh8TXpVzzBDZiH1kWPWSs
+gW9YBu7/p/22U4++X23RyaheGuysfRAMv9cTv+8T0J8NHaAmQz4/QHFXh+0/tQgU
+EVQVGDF6KNU=
+-----END CERTIFICATE-----
+`)
+
+var _ = Describe("attribute", func() {
+ var cfgctx config.Context
+ var ctx me.Context
+
+ BeforeEach(func() {
+ cfgctx = config.New(datacontext.MODE_DEFAULTED)
+ ctx = cfgctx.AttributesContext()
+ })
+
+ It("marshal/unmarshal", func() {
+ cfg := me.New()
+ cfg.AddRootCertificateData(certdata)
+
+ data, err := json.Marshal(cfg)
+ Expect(err).To(Succeed())
+
+ r := &me.Config{}
+ Expect(json.Unmarshal(data, r)).To(Succeed())
+ Expect(r).To(Equal(cfg))
+ })
+
+ It("applies root certificate", func() {
+ cfg := me.New()
+ cfg.AddRootCertificateData(certdata)
+
+ Expect(cfgctx.ApplyConfig(cfg, "from test")).To(Succeed())
+ Expect(me.Get(ctx).HasRootCertificates()).To(BeTrue())
+ })
+})
diff --git a/api/datacontext/attrs/rootcertsattr/config.go b/api/datacontext/attrs/rootcertsattr/config.go
new file mode 100644
index 000000000..4a2fd9b3b
--- /dev/null
+++ b/api/datacontext/attrs/rootcertsattr/config.go
@@ -0,0 +1,108 @@
+package rootcertsattr
+
+import (
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/tech/signing/signutils"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "rootcerts" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+// Config describes a memory based repository interface.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ RootCertificates []cfgcpi.ContentSpec `json:"rootCertificates,omitempty"`
+}
+
+// New creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) AddRootCertificateFile(name string, fss ...vfs.FileSystem) {
+ a.RootCertificates = append(a.RootCertificates, cfgcpi.ContentSpec{Path: name, FileSystem: utils.Optional(fss...)})
+}
+
+func (a *Config) AddRootCertificateData(data []byte) {
+ a.RootCertificates = append(a.RootCertificates, cfgcpi.ContentSpec{Data: data})
+}
+
+func (a *Config) AddRootCertificate(chain signutils.GenericCertificateChain) error {
+ certs, err := signutils.GetCertificateChain(chain, false)
+ if err != nil {
+ return err
+ }
+ a.RootCertificates = append(a.RootCertificates, cfgcpi.ContentSpec{Data: signutils.CertificateChainToPem(certs), Parsed: certs})
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ if t, ok := target.(Context); ok {
+ if t.AttributesContext().IsAttributesContext() { // apply only to root context
+ return errors.Wrapf(a.ApplyToAttribute(Get(t)), "applying config to certattr failed")
+ }
+ }
+ return cfgcpi.ErrNoContext(ConfigType)
+}
+
+func (a *Config) ApplyToAttribute(attr *Attribute) error {
+ for i, k := range a.RootCertificates {
+ err := attr.RegisterRootCertificates(k)
+ if err != nil {
+ return errors.Wrapf(err, "invalid certificate %d", i)
+ }
+ }
+ return nil
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define
+general root certificates. A certificate value might be given by one of the fields:
+- path
: path of file with key data
+- data
: base64 encoded binary data
+- stringdata
: data a string parsed by key handler
+
++ rootCertificates: + - path: <file path> ++ +` + +type Appliers struct { + lock sync.Mutex + appliers []cfgcpi.ConfigApplier +} + +func (r *Appliers) Register(a ...cfgcpi.ConfigApplier) { + r.lock.Lock() + defer r.lock.Unlock() + + r.appliers = append(r.appliers, a...) +} + +var DefaultAppliers = &Appliers{} + +func RegisterApplier(a ...cfgcpi.ConfigApplier) { + DefaultAppliers.Register(a...) +} diff --git a/pkg/contexts/datacontext/attrs/rootcertsattr/suite_test.go b/api/datacontext/attrs/rootcertsattr/suite_test.go similarity index 100% rename from pkg/contexts/datacontext/attrs/rootcertsattr/suite_test.go rename to api/datacontext/attrs/rootcertsattr/suite_test.go diff --git a/api/datacontext/attrs/tmpcache/attr.go b/api/datacontext/attrs/tmpcache/attr.go new file mode 100644 index 000000000..733589ecf --- /dev/null +++ b/api/datacontext/attrs/tmpcache/attr.go @@ -0,0 +1,104 @@ +package tmpcache + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/tempblobcache" + ATTR_SHORT = "blobcache" +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*string* Foldername for temporary blob cache +The temporary blob cache is used to accessing large blobs from remote sytems. +The are temporarily stored in the filesystem, instead of the memory, to avoid +blowing up the memory consumption. +` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if a, ok := v.(*Attribute); !ok { + return nil, fmt.Errorf("temppcache attribute") + } else { + return []byte(a.Path), nil + } +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + var s string + err := runtime.DefaultYAMLEncoding.Unmarshal(data, &s) + if err != nil { + return nil, errors.Wrapf(err, "invalid attribute value for %s", ATTR_KEY) + } + return &Attribute{ + Path: s, + }, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +type Attribute struct { + Path string + Filesystem vfs.FileSystem +} + +func New(path string, fss ...vfs.FileSystem) *Attribute { + return &Attribute{ + Path: path, + Filesystem: utils.FileSystem(fss...), + } +} + +func (a *Attribute) CreateTempFile(pat string) (vfs.File, error) { + err := a.Filesystem.MkdirAll(a.Path, 0o777) + if err != nil { + return nil, err + } + return vfs.TempFile(a.Filesystem, a.Path, pat) +} + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctx datacontext.Context) *Attribute { + var v interface{} + var fs vfs.FileSystem + + if ctx != nil { + v = ctx.GetAttributes().GetAttribute(ATTR_KEY) + fs = utils.FileSystem(vfsattr.Get(ctx)) + } + fs = utils.FileSystem(fs) + + if v != nil { + a := v.(*Attribute) + if a.Filesystem == nil { + a.Filesystem = fs + } + return a + } + return &Attribute{fs.FSTempDir(), fs} +} + +func Set(ctx datacontext.Context, a *Attribute) { + ctx.GetAttributes().SetAttribute(ATTR_KEY, a) +} diff --git a/api/datacontext/attrs/vfsattr/attr.go b/api/datacontext/attrs/vfsattr/attr.go new file mode 100644 index 000000000..6a1c14b45 --- /dev/null +++ b/api/datacontext/attrs/vfsattr/attr.go @@ -0,0 +1,62 @@ +package vfsattr + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/vfs" + ATTR_SHORT = "vfs" +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*intern* (not via command line) +Virtual filesystem to use for command line context. +` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if _, ok := v.(vfs.FileSystem); !ok { + return nil, fmt.Errorf("vfs.CachingFileSystem required") + } + return nil, nil +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + return nil, errors.ErrNotSupported("decode attribute", ATTR_KEY) +} + +//////////////////////////////////////////////////////////////////////////////// + +var _osfs = osfs.New() + +func Get(ctx datacontext.Context) vfs.FileSystem { + v := ctx.GetAttributes().GetAttribute(ATTR_KEY) + if v == nil { + return _osfs + } + fs, _ := v.(vfs.FileSystem) + return fs +} + +func Set(ctx datacontext.Context, fs vfs.FileSystem) { + ctx.GetAttributes().SetAttribute(ATTR_KEY, fs) +} diff --git a/api/datacontext/builder.go b/api/datacontext/builder.go new file mode 100644 index 000000000..4307c0296 --- /dev/null +++ b/api/datacontext/builder.go @@ -0,0 +1,63 @@ +package datacontext + +import ( + "context" + + "ocm.software/ocm/api/datacontext/action/api" + "ocm.software/ocm/api/datacontext/action/handlers" +) + +type Builder struct { + ctx context.Context + attributes Attributes + actions handlers.Registry +} + +func (b *Builder) getContext() context.Context { + if b.ctx == nil { + return context.Background() + } + return b.ctx +} + +func (b Builder) WithContext(ctx context.Context) Builder { + b.ctx = ctx + return b +} + +func (b Builder) WithAttributes(paranetAttr Attributes) Builder { + b.attributes = paranetAttr + return b +} + +func (b Builder) WithActionHandlers(hdlrs handlers.Registry) Builder { + b.actions = hdlrs + return b +} + +func (b Builder) Bound() (Context, context.Context) { + c := b.New() + return c, context.WithValue(b.getContext(), key, c) +} + +func (b Builder) New(m ...BuilderMode) Context { + mode := Mode(m...) + + if b.actions == nil { + switch mode { + case MODE_INITIAL: + b.actions = handlers.NewRegistry(api.NewActionTypeRegistry()) + case MODE_CONFIGURED: + b.actions = handlers.NewRegistry(api.DefaultRegistry().Copy()) + handlers.DefaultRegistry().AddTo(b.actions) + case MODE_EXTENDED: + b.actions = handlers.NewRegistry(api.DefaultRegistry(), handlers.DefaultRegistry()) + case MODE_DEFAULTED: + fallthrough + case MODE_SHARED: + b.actions = handlers.DefaultRegistry() + } + } + + return newWithActions(mode, nil, b.actions) +} diff --git a/api/datacontext/config/attrs/config_test.go b/api/datacontext/config/attrs/config_test.go new file mode 100644 index 000000000..cc43f4ba4 --- /dev/null +++ b/api/datacontext/config/attrs/config_test.go @@ -0,0 +1,80 @@ +package attrs_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext" + local "ocm.software/ocm/api/datacontext/config/attrs" + "ocm.software/ocm/api/utils/runtime" +) + +const ATTR_KEY = "test" + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +A Test attribute. +` +} + +type Attribute struct { + Value string `json:"value"` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if _, ok := v.(*Attribute); !ok { + return nil, fmt.Errorf("boolean required") + } + return marshaller.Marshal(v) +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + var value Attribute + err := unmarshaller.Unmarshal(data, &value) + return &value, err +} + +//////////////////////////////////////////////////////////////////////////////// + +var _ = Describe("generic attributes", func() { + attribute := &Attribute{"TEST"} + var ctx config.Context + + BeforeEach(func() { + ctx = config.WithSharedAttributes(datacontext.New(nil)).New() + }) + + Context("applies", func() { + It("applies later attribute config", func() { + sub := credentials.WithConfigs(ctx).New() + spec := local.New() + Expect(spec.AddAttribute(ATTR_KEY, attribute)).To(Succeed()) + Expect(ctx.ApplyConfig(spec, "test")).To(Succeed()) + + Expect(sub.GetAttributes().GetAttribute(ATTR_KEY, nil)).To(Equal(attribute)) + }) + + It("applies earlier attribute config", func() { + spec := local.New() + Expect(spec.AddAttribute(ATTR_KEY, attribute)).To(Succeed()) + Expect(ctx.ApplyConfig(spec, "test")).To(Succeed()) + + sub := credentials.WithConfigs(ctx).New() + Expect(sub.GetAttributes().GetAttribute(ATTR_KEY, nil)).To(Equal(attribute)) + }) + }) +}) diff --git a/pkg/contexts/datacontext/config/attrs/suite_test.go b/api/datacontext/config/attrs/suite_test.go similarity index 100% rename from pkg/contexts/datacontext/config/attrs/suite_test.go rename to api/datacontext/config/attrs/suite_test.go diff --git a/api/datacontext/config/attrs/type.go b/api/datacontext/config/attrs/type.go new file mode 100644 index 000000000..d6f8b841c --- /dev/null +++ b/api/datacontext/config/attrs/type.go @@ -0,0 +1,87 @@ +package attrs + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + + cfgcpi "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ConfigType = "attributes" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX + ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" +) + +func init() { + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage)) + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage)) +} + +// Config describes a memory based repository interface. +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + // Attributes descibe a set of geeric attribute settings + Attributes map[string]json.RawMessage `json:"attributes,omitempty"` +} + +// New creates a new memory ConfigSpec. +func New() *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), + Attributes: map[string]json.RawMessage{}, + } +} + +func (a *Config) GetType() string { + return ConfigType +} + +func (a *Config) AddAttribute(attr string, value interface{}) error { + data, err := datacontext.DefaultAttributeScheme.Encode(attr, value, runtime.DefaultJSONEncoding) + if err == nil { + a.Attributes[attr] = data + } + return err +} + +func (a *Config) AddRawAttribute(attr string, data []byte) error { + _, err := datacontext.DefaultAttributeScheme.Decode(attr, data, runtime.DefaultJSONEncoding) + if err == nil { + a.Attributes[attr] = data + } + return err +} + +func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { + list := errors.ErrListf("applying config") + t, ok := target.(cfgcpi.Context) + if !ok { + return cfgcpi.ErrNoContext(ConfigType) + } + if a.Attributes == nil { + return nil + } + for a, e := range a.Attributes { + eff := datacontext.DefaultAttributeScheme.Shortcuts()[a] + if eff != "" { + a = eff + } + list.Add(errors.Wrapf(t.GetAttributes().SetEncodedAttribute(a, e, runtime.DefaultJSONEncoding), "attribute %q", a)) + } + return list.Result() +} + +const usage = ` +The config type
` + ConfigType + `
can be used to define a list
+of arbitrary attribute specifications:
+
++ type: ` + ConfigType + ` + attributes: + <name>: <yaml defining the attribute> + ... ++` diff --git a/api/datacontext/config/init.go b/api/datacontext/config/init.go new file mode 100644 index 000000000..8655415cd --- /dev/null +++ b/api/datacontext/config/init.go @@ -0,0 +1,6 @@ +package config + +import ( + _ "ocm.software/ocm/api/datacontext/config/attrs" + _ "ocm.software/ocm/api/datacontext/config/logging" +) diff --git a/api/datacontext/config/logging/config_test.go b/api/datacontext/config/logging/config_test.go new file mode 100644 index 000000000..7b8e10ffe --- /dev/null +++ b/api/datacontext/config/logging/config_test.go @@ -0,0 +1,240 @@ +package logging_test + +import ( + "bytes" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/utils/logging/testhelper" + + "github.com/mandelsoft/logging" + "github.com/tonglil/buflogr" + + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/datacontext" + logcfg "ocm.software/ocm/api/datacontext/config/logging" + log "ocm.software/ocm/api/utils/logging" +) + +var _ = Describe("logging configuration", func() { + var ctx datacontext.AttributesContext + var cfg config.Context + var buf bytes.Buffer + var orig logging.Context + + BeforeEach(func() { + orig = logging.DefaultContext().(*logging.ContextReference).Context + logging.SetDefaultContext(logging.NewDefault()) + log.SetContext(nil) + ctx = datacontext.New(nil) + cfg = config.WithSharedAttributes(ctx).New() + + buf.Reset() + def := buflogr.NewWithBuffer(&buf) + ctx.LoggingContext().SetBaseLogger(def) + }) + + AfterEach(func() { + // logging.SetDefaultContext(orig) + }) + _ = cfg + _ = orig + + It("just logs with defaults", func() { + LogTest(ctx) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +V[3] info realm ocm +V[2] warn realm ocm +ERROR
` + ConfigType + `
can be used to configure the logging
+aspect of a dedicated context type:
+
++ type: ` + ConfigType + ` + contextType: ` + datacontext.CONTEXT_TYPE + ` + settings: + defaultLevel: Info + rules: + - ... ++ +The context type ` + datacontext.CONTEXT_TYPE + ` is the root context of a +context hierarchy. + +If no context type is specified, the config will be applies to any target +acting as logging context provider, which is not a non-root context. +` diff --git a/pkg/contexts/datacontext/context-refcount-model.png b/api/datacontext/context-refcount-model.png similarity index 100% rename from pkg/contexts/datacontext/context-refcount-model.png rename to api/datacontext/context-refcount-model.png diff --git a/api/datacontext/context.go b/api/datacontext/context.go new file mode 100644 index 000000000..d883bec95 --- /dev/null +++ b/api/datacontext/context.go @@ -0,0 +1,412 @@ +package datacontext + +import ( + "context" + "fmt" + "io" + "reflect" + "sync" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/logging" + + "ocm.software/ocm/api/datacontext/action/handlers" + "ocm.software/ocm/api/utils" + ocmlog "ocm.software/ocm/api/utils/logging" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/refmgmt" + "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/api/utils/runtimefinalizer" +) + +const OCM_CONTEXT_SUFFIX = ".context" + common.OCM_TYPE_GROUP_SUFFIX + +// BuilderMode controls the handling of unset information in the +// builder configuration when calling the New method. +type BuilderMode int + +const ( + // MODE_SHARED uses the default contexts for unset nested context types. + MODE_SHARED BuilderMode = iota + // MODE_DEFAULTED uses dedicated context instances configured with the + // context type specific default registrations. + MODE_DEFAULTED + // MODE_EXTENDED uses dedicated context instances configured with + // context type registrations extending the default registrations. + MODE_EXTENDED + // MODE_CONFIGURED uses dedicated context instances configured with the + // context type registrations configured with the actual state of the + // default registrations. + MODE_CONFIGURED + // MODE_INITIAL uses completely new contexts for unset nested context types + // and initial registrations. + MODE_INITIAL +) + +const MULTI_REF = false + +func (m BuilderMode) String() string { + switch m { + case MODE_SHARED: + return "shared" + case MODE_DEFAULTED: + return "defaulted" + case MODE_EXTENDED: + return "extended" + case MODE_CONFIGURED: + return "configured" + case MODE_INITIAL: + return "initial" + default: + return fmt.Sprintf("(invalid %d)", m) + } +} + +func Mode(m ...BuilderMode) BuilderMode { + return utils.OptionalDefaulted(MODE_EXTENDED, m...) +} + +type ContextIdentity = runtimefinalizer.ObjectIdentity + +type ContextProvider interface { + // AttributesContext returns the shared attributes + AttributesContext() AttributesContext +} + +// Delegates is the interface for common +// Context features, which might be delegated +// to aggregated contexts. +type Delegates interface { + ocmlog.LogProvider + handlers.ActionsProvider +} + +type _delegates struct { + logging logging.Context + actions handlers.Registry +} + +func (d _delegates) LoggingContext() logging.Context { + return d.logging +} + +func (d _delegates) AttributionContext() logging.AttributionContext { + return d.logging.AttributionContext() +} + +func (d _delegates) Logger(messageContext ...logging.MessageContext) logging.Logger { + return d.logging.Logger(messageContext) +} + +func (d _delegates) GetActions() handlers.Registry { + return d.actions +} + +func ComposeDelegates(l logging.Context, a handlers.Registry) Delegates { + return _delegates{l, a} +} + +type ContextBinder interface { + // BindTo binds the context to a context.Context and makes it + // retrievable by a ForContext method + BindTo(ctx context.Context) context.Context +} + +// Context describes a common interface for a data context used for a dedicated +// purpose. +// Such has a type and always specific attribute store. +// Every Context can be bound to a context.Context. +type Context interface { + ContextBinder + ContextProvider + Delegates + + IsIdenticalTo(Context) bool + + // GetType returns the context type + GetType() string + GetId() ContextIdentity + + GetAttributes() Attributes + + Finalize() error + Finalizer() *finalizer.Finalizer +} + +type InternalContext interface { + Context + runtimefinalizer.RecorderProvider + GetKey() interface{} + GetAllocatable() refmgmt.Allocatable +} + +//////////////////////////////////////////////////////////////////////////////// + +// CONTEXT_TYPE is the global type for an attribute context. +const CONTEXT_TYPE = "attributes" + OCM_CONTEXT_SUFFIX + +type AttributesContext interface { + Context + + IsAttributesContext() bool + AttributesContext() AttributesContext + + BindTo(ctx context.Context) context.Context +} + +// AttributeFactory is used to atomicly create a new attribute for a context. +type AttributeFactory func(Context) interface{} + +type Attributes interface { + finalizer.Finalizable + + GetAttribute(name string, def ...interface{}) interface{} + SetAttribute(name string, value interface{}) error + SetEncodedAttribute(name string, data []byte, unmarshaller runtime.Unmarshaler) error + GetOrCreateAttribute(name string, creator AttributeFactory) interface{} +} + +// DefaultContext is the default context initialized by init functions. +var DefaultContext = NewWithActions(nil, handlers.DefaultRegistry()) + +// ForContext returns the Context to use for context.Context. +// This is either an explicit context or the default context. +func ForContext(ctx context.Context) AttributesContext { + c, _ := ForContextByKey(ctx, key, DefaultContext) + if c == nil { + return nil + } + return c.(AttributesContext) +} + +// WithContext create a new Context bound to a context.Context. +func WithContext(ctx context.Context, parentAttrs Attributes) (Context, context.Context) { + c := New(parentAttrs) + return c, c.BindTo(ctx) +} + +//////////////////////////////////////////////////////////////////////////////// + +type Updater interface { + Update() error +} + +type UpdateFunc func() error + +func (u UpdateFunc) Update() error { + return u() +} + +type delegates = Delegates + +//////////////////////////////////////////////////////////////////////////////// + +var key = reflect.TypeOf(_context{}) + +type _context struct { + *contextBase + updater Updater +} + +// gcWrapper is used as garbage collectable +// wrapper for a context implementation +// to establish a runtime finalizer. +type gcWrapper struct { + GCWrapper + *_context +} + +func newView(c *_context, ref ...bool) AttributesContext { + if utils.Optional(ref...) { + return FinalizedContext[gcWrapper](c) + } + return c +} + +func (w *gcWrapper) SetContext(c *_context) { + w._context = c +} + +var ( + _ Context = (*_context)(nil) + _ ViewCreator[AttributesContext] = (*_context)(nil) +) + +// New provides a root attribute context. +func New(parentAttrs ...Attributes) AttributesContext { + return NewWithActions(utils.Optional(parentAttrs...), handlers.NewRegistry(nil, handlers.DefaultRegistry())) +} + +func NewWithActions(parentAttrs Attributes, actions handlers.Registry) AttributesContext { + return newWithActions(MODE_DEFAULTED, parentAttrs, actions) +} + +func newWithActions(mode BuilderMode, parentAttrs Attributes, actions handlers.Registry) AttributesContext { + c := &_context{} + c.contextBase = newContextBase(c, CONTEXT_TYPE, key, parentAttrs, &c.updater, + ComposeDelegates(logging.NewWithBase(ocmlog.Context()), handlers.NewRegistry(nil, actions)), + ) + return SetupContext(mode, c.CreateView()) // see above +} + +func (c *_context) CreateView() AttributesContext { + return newView(c, true) +} + +func (c *_context) AttributesContext() AttributesContext { + if c.updater != nil { + c.updater.Update() + } + return newView(c) +} + +func (c *_context) IsAttributesContext() bool { + return true +} + +func (c *_context) Actions() handlers.Registry { + if c.updater != nil { + c.updater.Update() + } + return c.contextBase.GetActions() +} + +func (c *_context) LoggingContext() logging.Context { + if c.updater != nil { + c.updater.Update() + } + return c.contextBase.LoggingContext() +} + +func (c *_context) Logger(messageContext ...logging.MessageContext) logging.Logger { + if c.updater != nil { + c.updater.Update() + } + return c.contextBase.Logger(messageContext...) +} + +//////////////////////////////////////////////////////////////////////////////// + +var contextrange, attrsrange = runtimefinalizer.NumberRange{}, runtimefinalizer.NumberRange{} + +type _attributes struct { + sync.RWMutex + id uint64 + ctx Context + parent Attributes + updater *Updater + attributes map[string]interface{} +} + +var _ Attributes = &_attributes{} + +func NewAttributes(ctx Context, parent Attributes, updater *Updater) Attributes { + return newAttributes(ctx, parent, updater) +} + +func newAttributes(ctx Context, parent Attributes, updater *Updater) *_attributes { + return &_attributes{ + id: attrsrange.NextId(), + ctx: ctx, + parent: parent, + updater: updater, + attributes: map[string]interface{}{}, + } +} + +func (c *_attributes) Finalize() error { + list := errors.ErrListf("finalizing attributes") + for n, a := range c.attributes { + if f, ok := a.(finalizer.Finalizable); ok { + list.Addf(nil, f.Finalize(), "attribute %s", n) + } + } + return list.Result() +} + +func (c *_attributes) GetAttribute(name string, def ...interface{}) interface{} { + if *c.updater != nil { + (*c.updater).Update() + } + c.RLock() + defer c.RUnlock() + if a := c.attributes[name]; a != nil { + return a + } + if c.parent != nil { + if a := c.parent.GetAttribute(name); a != nil { + return a + } + } + return utils.Optional(def...) +} + +func (c *_attributes) SetEncodedAttribute(name string, data []byte, unmarshaller runtime.Unmarshaler) error { + s := DefaultAttributeScheme.Shortcuts()[name] + if s != "" { + name = s + } + v, err := DefaultAttributeScheme.Decode(name, data, unmarshaller) + if err != nil { + return err + } + c.SetAttribute(name, v) + return nil +} + +func (c *_attributes) setAttribute(name string, value interface{}) error { + c.Lock() + defer c.Unlock() + + _, err := DefaultAttributeScheme.Encode(name, value, nil) + if err != nil && !errors.IsErrUnknownKind(err, "attribute") { + return err + } + old := c.attributes[name] + if old != nil && old != value { + if c, ok := old.(io.Closer); ok { + c.Close() + } + } + value, err = DefaultAttributeScheme.Convert(name, value) + if err != nil && !errors.IsErrUnknownKind(err, "attribute") { + return err + } + c.attributes[name] = value + return nil +} + +func (c *_attributes) SetAttribute(name string, value interface{}) error { + err := c.setAttribute(name, value) + if err == nil { + if *c.updater != nil { + (*c.updater).Update() + } + } + return err +} + +func (c *_attributes) getOrCreateAttribute(name string, creator AttributeFactory) interface{} { + c.Lock() + defer c.Unlock() + if v := c.attributes[name]; v != nil { + return v + } + if c.parent != nil { + if v := c.parent.GetAttribute(name); v != nil { + return v + } + } + v := creator(c.ctx) + c.attributes[name] = v + return v +} + +func (c *_attributes) GetOrCreateAttribute(name string, creator AttributeFactory) interface{} { + r := c.getOrCreateAttribute(name, creator) + if *c.updater != nil { + (*c.updater).Update() + } + return r +} diff --git a/api/datacontext/context_test.go b/api/datacontext/context_test.go new file mode 100644 index 000000000..26c1be014 --- /dev/null +++ b/api/datacontext/context_test.go @@ -0,0 +1,23 @@ +package datacontext_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + me "ocm.software/ocm/api/datacontext" +) + +var _ = Describe("area test", func() { + It("can be garbage collected", func() { + // ocmlog.Context().AddRule(logging.NewConditionRule(logging.DebugLevel, me.Realm)) + + ctx := me.New() + Expect(ctx.IsIdenticalTo(ctx)).To(BeTrue()) + + ctx2 := ctx.AttributesContext() + Expect(ctx.IsIdenticalTo(ctx2)).To(BeTrue()) + + ctx3 := me.New() + Expect(ctx.IsIdenticalTo(ctx3)).To(BeFalse()) + }) +}) diff --git a/pkg/contexts/datacontext/cpi.go b/api/datacontext/cpi.go similarity index 96% rename from pkg/contexts/datacontext/cpi.go rename to api/datacontext/cpi.go index 4598e0781..42fe2f320 100644 --- a/pkg/contexts/datacontext/cpi.go +++ b/api/datacontext/cpi.go @@ -9,10 +9,10 @@ import ( "github.com/mandelsoft/logging" "github.com/modern-go/reflect2" - "github.com/open-component-model/ocm/pkg/contexts/datacontext/action/handlers" - "github.com/open-component-model/ocm/pkg/refmgmt" - "github.com/open-component-model/ocm/pkg/refmgmt/finalized" - "github.com/open-component-model/ocm/pkg/runtimefinalizer" + "ocm.software/ocm/api/datacontext/action/handlers" + "ocm.software/ocm/api/utils/refmgmt" + "ocm.software/ocm/api/utils/refmgmt/finalized" + "ocm.software/ocm/api/utils/runtimefinalizer" ) // NewContextBase creates a context base implementation supporting diff --git a/api/datacontext/gc_test.go b/api/datacontext/gc_test.go new file mode 100644 index 000000000..56bb08144 --- /dev/null +++ b/api/datacontext/gc_test.go @@ -0,0 +1,92 @@ +package datacontext_test + +import ( + "runtime" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/general" + + me "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/utils/runtimefinalizer" +) + +var _ = Describe("area test", func() { + It("can be garbage collected", func() { + ctx := me.New() + r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx) + id := ctx.GetId() + Expect(me.GetContextRefCount(ctx)).To(Equal(1)) + ctx = nil + runtime.GC() + time.Sleep(time.Second) + Expect(r.Get()).To(ConsistOf(id)) + }) + + It("provides second reference", func() { + // ocmlog.Context().AddRule(logging.NewConditionRule(logging.DebugLevel, me.Realm)) + multiRefs := general.Conditional(me.MULTI_REF, 2, 1) + + ctx := me.New() + Expect(me.GetContextRefCount(ctx)).To(Equal(1)) + + actx := ctx.AttributesContext() + Expect(me.GetContextRefCount(ctx)).To(Equal(multiRefs)) + + r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx) + Expect(r).NotTo(BeNil()) + + runtime.GC() + time.Sleep(time.Second) + ctx.GetType() + Expect(r.Get()).To(BeNil()) + + actx.GetType() + actx = nil + runtime.GC() + time.Sleep(time.Second) + ctx.GetType() + Expect(r.Get()).To(BeNil()) + Expect(me.GetContextRefCount(ctx)).To(Equal(1)) + + ctx = nil + for i := 0; i < 100; i++ { + runtime.GC() + time.Sleep(time.Millisecond) + } + + Expect(r.Get()).To(ContainElement(ContainSubstring(me.CONTEXT_TYPE))) + }) + + It("creates views", func() { + ctx := me.New() + r := runtimefinalizer.GetRuntimeFinalizationRecorder(ctx) + + Expect(me.GetContextRefCount(ctx)).To(Equal(1)) + Expect(me.IsPersistentContextRef(ctx)).To(BeTrue()) + + view := me.PersistentContextRef(ctx) + Expect(me.GetContextRefCount(view)).To(Equal(1)) // reuse persistent ref + Expect(me.IsPersistentContextRef(view)).To(BeTrue()) + + non := view.AttributesContext() + Expect(me.IsPersistentContextRef(non)).To(BeFalse()) + + view2 := me.PersistentContextRef(non) + Expect(me.GetContextRefCount(view2)).To(Equal(2)) // create new view + Expect(me.IsPersistentContextRef(view2)).To(BeTrue()) + + Expect(ctx.IsIdenticalTo(view)).To(BeTrue()) + Expect(ctx.IsIdenticalTo(view2)).To(BeTrue()) + + ctx = nil + view = nil + view2 = nil + + runtime.GC() + time.Sleep(time.Second) + Expect(len(r.Get())).To(Equal(1)) // ref non is not persistent + }) +}) diff --git a/api/datacontext/logging.go b/api/datacontext/logging.go new file mode 100644 index 000000000..6c0cac83c --- /dev/null +++ b/api/datacontext/logging.go @@ -0,0 +1,13 @@ +package datacontext + +import ( + ocmlog "ocm.software/ocm/api/utils/logging" +) + +var Realm = ocmlog.DefineSubRealm("context lifecycle", "context") + +var Logger = ocmlog.DynamicLogger(Realm) + +func Debug(c Context, msg string, keypairs ...interface{}) { + c.LoggingContext().Logger(Realm).Debug(msg, append(keypairs, "id", c.GetId())...) +} diff --git a/api/datacontext/session.go b/api/datacontext/session.go new file mode 100644 index 000000000..c6e790dbe --- /dev/null +++ b/api/datacontext/session.go @@ -0,0 +1,149 @@ +package datacontext + +import ( + "io" + "sync" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/utils/accessio" +) + +// Session is a context keeping track of objects requiring a close +// after final use. When closing a session all subsequent objects +// will be closed in the opposite order they are added. +// Added closers may be closed prio to the session without causing +// errors. +type Session interface { + // Closer adds a closer returned by a function call providing a closer and an error + // to the session if not error is returned. The results of the call are forwarded to + // the own result. Unfortunately, Go does not support type parameters for methods, + // therefore only an io.Closer can be returned a function result. + Closer(closer io.Closer, extra ...interface{}) (io.Closer, error) + GetOrCreate(key interface{}, creator func(SessionBase) Session) Session + AddCloser(closer io.Closer, callbacks ...accessio.CloserCallback) io.Closer + Close() error + IsClosed() bool +} + +type SessionBase interface { + Lock() + Unlock() + RLock() + RUnlock() + + Session() Session + IsClosed() bool + AddCloser(closer io.Closer, callbacks ...accessio.CloserCallback) io.Closer +} + +type ObjectKey struct { + Object interface{} + Name string +} + +type session struct { + base sessionBase +} + +type sessionBase struct { + sync.RWMutex + session Session + closed bool + closer []io.Closer + sessions map[interface{}]Session +} + +func NewSession() Session { + s := &session{ + sessionBase{ + sessions: map[interface{}]Session{}, + }, + } + s.base.session = s + return s +} + +func GetOrCreateSubSession(s Session, key interface{}, creator func(SessionBase) Session) Session { + if s == nil { + s = NewSession() + } + return s.GetOrCreate(key, creator) +} + +func (s *session) IsClosed() bool { + s.base.RLock() + defer s.base.RUnlock() + return s.base.closed +} + +func (s *session) Close() error { + s.base.Lock() + defer s.base.Unlock() + return s.base.Close() +} + +func (s *session) Closer(closer io.Closer, extra ...interface{}) (io.Closer, error) { + for _, e := range extra { + if err, ok := e.(error); ok && err != nil { + return nil, err + } + } + if closer == nil { + return nil, nil + } + s.base.Lock() + defer s.base.Unlock() + s.base.AddCloser(closer) + + return closer, nil +} + +func (s *session) AddCloser(closer io.Closer, callbacks ...accessio.CloserCallback) io.Closer { + if closer == nil { + return nil + } + s.base.Lock() + defer s.base.Unlock() + return s.base.AddCloser(closer, callbacks...) +} + +func (s *session) GetOrCreate(key interface{}, creator func(SessionBase) Session) Session { + s.base.Lock() + defer s.base.Unlock() + return s.base.GetOrCreate(key, creator) +} + +func (s *sessionBase) Session() Session { + return s.session +} + +func (s *sessionBase) IsClosed() bool { + return s.closed +} + +func (s *sessionBase) Close() error { + if s.closed { + return nil + } + s.closed = true + list := errors.ErrListf("closing session") + for i := len(s.closer) - 1; i >= 0; i-- { + list.Add(s.closer[i].Close()) + } + return list.Result() +} + +func (s *sessionBase) AddCloser(closer io.Closer, callbacks ...accessio.CloserCallback) io.Closer { + s.closer = append(s.closer, accessio.OnceCloser(closer, callbacks...)) + return closer +} + +func (s *sessionBase) GetOrCreate(key interface{}, creator func(SessionBase) Session) Session { + cur := s.sessions[key] + if cur == nil { + cur = creator(s) + s.sessions[key] = cur + } + return cur +} diff --git a/pkg/contexts/datacontext/setup.go b/api/datacontext/setup.go similarity index 100% rename from pkg/contexts/datacontext/setup.go rename to api/datacontext/setup.go diff --git a/pkg/contexts/datacontext/suite_test.go b/api/datacontext/suite_test.go similarity index 100% rename from pkg/contexts/datacontext/suite_test.go rename to api/datacontext/suite_test.go diff --git a/pkg/contexts/datacontext/util.go b/api/datacontext/util.go similarity index 100% rename from pkg/contexts/datacontext/util.go rename to api/datacontext/util.go diff --git a/pkg/env/builder/blob.go b/api/helper/builder/blob.go similarity index 87% rename from pkg/env/builder/blob.go rename to api/helper/builder/blob.go index 894e7c8a2..c35252c56 100644 --- a/pkg/env/builder/blob.go +++ b/api/helper/builder/blob.go @@ -1,9 +1,9 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/blobaccess" - "github.com/open-component-model/ocm/pkg/blobaccess/dirtree" - "github.com/open-component-model/ocm/pkg/utils" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/blobaccess" + "ocm.software/ocm/api/utils/blobaccess/dirtree" ) const T_BLOBACCESS = "blob access" diff --git a/api/helper/builder/builder.go b/api/helper/builder/builder.go new file mode 100644 index 000000000..afaf600ad --- /dev/null +++ b/api/helper/builder/builder.go @@ -0,0 +1,210 @@ +package builder + +import ( + "github.com/mandelsoft/goutils/exception" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/modern-go/reflect2" + "github.com/onsi/ginkgo/v2" + + "ocm.software/ocm/api/helper/env" + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +type element interface { + SetBuilder(b *Builder) + Type() string + Close() error + Set() + + Result() interface{} +} + +type State struct{} + +type base struct { + *Builder + result interface{} +} + +func (e *base) SetBuilder(b *Builder) { + e.Builder = b +} + +func (e *base) Result() interface{} { + return e.result +} + +type static struct { + def_modopts ocm.ModificationOptions +} + +type state struct { + *static + ocm_repo ocm.Repository + ocm_comp ocm.ComponentAccess + ocm_vers ocm.ComponentVersionAccess + ocm_rsc *compdesc.ResourceMeta + ocm_src *compdesc.SourceMeta + ocm_meta *compdesc.ElementMeta + ocm_labels *metav1.Labels + ocm_acc *compdesc.AccessSpec + ocm_modopts *ocm.ModificationOptions + + blob *blobaccess.BlobAccess + hint *string + + oci_repo oci.Repository + oci_nsacc oci.NamespaceAccess + oci_artacc oci.ArtifactAccess + oci_cleanuplayers bool + oci_tags *[]string + oci_artfunc func(oci.ArtifactAccess) error + oci_annofunc func(name, value string) + oci_platform *artdesc.Platform +} + +type Builder struct { + *env.Environment + stack []element + state +} + +// New creates a new composition environment +// including an own OCM context and a private +// filesystem, which can be used to compose +// OCM/OCI repositories and their content. +// It can be configured to work with dedicated +// settings, also. +func New(opts ...env.Option) *Builder { + return &Builder{Environment: env.NewEnvironment(append([]env.Option{env.FileSystem(osfs.OsFs, "/"), env.FailHandler(env.ExceptionFailHandler)}, opts...)...), state: state{static: &static{}}} +} + +// NewBuilder creates a new composition environment +// including an own OCM context and a private +// filesystem, which can be used to compose +// OCM/OCI repositories and their content. +// By default, a private environment is created based on +// a ginko fail handling intended to be used for test cases. +// But it can be configured to work as library with dedicated +// settings, also. +func NewBuilder(opts ...env.Option) *Builder { + return &Builder{Environment: env.NewEnvironment(append([]env.Option{env.FailHandler(ginkgo.Fail)}, opts...)...), state: state{static: &static{}}} +} + +var _ accessio.Option = (*Builder)(nil) + +// Build executes the given functions and returns a potential configuration +// error, instead of using the builder's env.FailHandler. +// Additionally, a build can always throw an exception using +// the exception.Throw function. +func (b *Builder) Build(funcs ...func(*Builder)) (err error) { + old := b.GetFailHandler() + defer func() { + b.SetFailHandler(old) + }() + b.SetFailHandler(env.ExceptionFailHandler) + + defer exception.PropagateException(&err) + for _, f := range funcs { + f(b) + } + return nil +} + +func (b *Builder) SetFailhandler(h ...env.FailHandler) *Builder { + b.Environment.SetFailHandler(h...) + return b +} + +// PropagateError can be used in defer to convert an composition error +// into an error return. +func (b *Builder) PropagateError(errp *error, matchers ...exception.Matcher) { + if r := recover(); r != nil { + *errp = exception.FilterException(r, matchers...) + } +} + +func (b *Builder) set() { + b.state = state{static: b.state.static} + + if len(b.stack) > 0 { + b.peek().Set() + } +} + +func (b *Builder) expect(p interface{}, msg string, tests ...func() bool) { + if reflect2.IsNil(p) { + b.fail(msg+" required", 1) + } + for _, f := range tests { + if !f() { + b.fail(msg+" required", 1) + } + } +} + +func (b *Builder) fail(msg string, callerSkip ...int) { + b.Fail(msg, utils.Optional(callerSkip...)+2) +} + +func (b *Builder) failOn(err error, callerSkip ...int) { + b.FailOnErr(err, "", utils.Optional(callerSkip...)+2) +} + +func (b *Builder) peek() element { + if len(b.stack) == 0 { + b.fail("no open frame", 2) + } + return b.stack[len(b.stack)-1] +} + +func (b *Builder) pop() element { + if len(b.stack) == 0 { + b.fail("no open frame", 2) + } + e := b.stack[len(b.stack)-1] + b.stack = b.stack[:len(b.stack)-1] + b.set() + return e +} + +func (b *Builder) push(e element) { + b.stack = append(b.stack, e) + b.set() +} + +func (b *Builder) configure(e element, funcs []func(), skip ...int) interface{} { + e.SetBuilder(b) + b.push(e) + b.Configure(funcs...) + err := b.pop().Close() + if err != nil { + b.fail(err.Error(), utils.Optional(skip...)+1) + } + return e.Result() +} + +func (b *Builder) Configure(funcs ...func()) { + for _, f := range funcs { + if f != nil { + f() + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +func (b *Builder) Hint(hint string) { + b.expect(b.hint, T_OCMACCESS) + if b.ocm_acc != nil && *b.ocm_acc != nil { + b.fail("access already set") + } + *(b.hint) = hint +} diff --git a/pkg/env/builder/builder_test.go b/api/helper/builder/builder_test.go similarity index 100% rename from pkg/env/builder/builder_test.go rename to api/helper/builder/builder_test.go diff --git a/pkg/env/builder/oci_anno.go b/api/helper/builder/oci_anno.go similarity index 100% rename from pkg/env/builder/oci_anno.go rename to api/helper/builder/oci_anno.go diff --git a/pkg/env/builder/oci_artifact.go b/api/helper/builder/oci_artifact.go similarity index 95% rename from pkg/env/builder/oci_artifact.go rename to api/helper/builder/oci_artifact.go index 7f03e7f84..e9794708f 100644 --- a/pkg/env/builder/oci_artifact.go +++ b/api/helper/builder/oci_artifact.go @@ -3,9 +3,9 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - "github.com/open-component-model/ocm/pkg/contexts/oci" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" ) const ( diff --git a/api/helper/builder/oci_artifactset.go b/api/helper/builder/oci_artifactset.go new file mode 100644 index 000000000..55a77fe93 --- /dev/null +++ b/api/helper/builder/oci_artifactset.go @@ -0,0 +1,20 @@ +package builder + +import ( + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +const T_OCIARTIFACTSET = "artifact set" + +//////////////////////////////////////////////////////////////////////////////// + +func (b *Builder) ArtifactSet(path string, fmt accessio.FileFormat, f ...func()) { + r, err := artifactset.Open(accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, path, 0o777, fmt, accessio.PathFileSystem(b.FileSystem())) + b.failOn(err) + + b.configure(&ociNamespace{NamespaceAccess: r, kind: T_OCIARTIFACTSET, annofunc: func(name, value string) { + r.Annotate(name, value) + }}, f) +} diff --git a/pkg/env/builder/oci_config.go b/api/helper/builder/oci_config.go similarity index 87% rename from pkg/env/builder/oci_config.go rename to api/helper/builder/oci_config.go index 875787d28..287d1528e 100644 --- a/pkg/env/builder/oci_config.go +++ b/api/helper/builder/oci_config.go @@ -3,8 +3,8 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) const T_OCICONFIG = "oci config" diff --git a/api/helper/builder/oci_ctf.go b/api/helper/builder/oci_ctf.go new file mode 100644 index 000000000..9832a58c7 --- /dev/null +++ b/api/helper/builder/oci_ctf.go @@ -0,0 +1,15 @@ +package builder + +import ( + "ocm.software/ocm/api/oci/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +const T_OCI_CTF = "oci common transport format" + +func (b *Builder) OCICommonTransport(path string, fmt accessio.FileFormat, f ...func()) { + r, err := ctf.Open(b.OCMContext().OCIContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, path, 0o777, accessio.PathFileSystem(b.FileSystem())) + b.failOn(err) + b.configure(&ociRepository{Repository: r, kind: T_OCI_CTF}, f) +} diff --git a/pkg/env/builder/oci_layer.go b/api/helper/builder/oci_layer.go similarity index 87% rename from pkg/env/builder/oci_layer.go rename to api/helper/builder/oci_layer.go index 694bd7247..64b90e503 100644 --- a/pkg/env/builder/oci_layer.go +++ b/api/helper/builder/oci_layer.go @@ -3,8 +3,8 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) const T_OCILAYER = "oci layer" diff --git a/pkg/env/builder/oci_namespace.go b/api/helper/builder/oci_namespace.go similarity index 92% rename from pkg/env/builder/oci_namespace.go rename to api/helper/builder/oci_namespace.go index e2d7eaf7c..d868a9c33 100644 --- a/pkg/env/builder/oci_namespace.go +++ b/api/helper/builder/oci_namespace.go @@ -1,7 +1,7 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/oci" + "ocm.software/ocm/api/oci" ) const T_OCINAMESPACE = "oci namespace" diff --git a/pkg/env/builder/oci_platform.go b/api/helper/builder/oci_platform.go similarity index 100% rename from pkg/env/builder/oci_platform.go rename to api/helper/builder/oci_platform.go diff --git a/pkg/env/builder/oci_repo.go b/api/helper/builder/oci_repo.go similarity index 78% rename from pkg/env/builder/oci_repo.go rename to api/helper/builder/oci_repo.go index 3fab3024d..e1d502620 100644 --- a/pkg/env/builder/oci_repo.go +++ b/api/helper/builder/oci_repo.go @@ -1,9 +1,9 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/oci" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" - "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ocireg" + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/extensions/repositories/ocireg" ) const T_OCIREPOSITORY = "oci repository" diff --git a/pkg/env/builder/oci_tags.go b/api/helper/builder/oci_tags.go similarity index 100% rename from pkg/env/builder/oci_tags.go rename to api/helper/builder/oci_tags.go diff --git a/pkg/env/builder/ocm_access.go b/api/helper/builder/ocm_access.go similarity index 85% rename from pkg/env/builder/ocm_access.go rename to api/helper/builder/ocm_access.go index c08a956e3..17e8f916a 100644 --- a/pkg/env/builder/ocm_access.go +++ b/api/helper/builder/ocm_access.go @@ -1,7 +1,7 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" + "ocm.software/ocm/api/ocm/compdesc" ) const T_OCMACCESS = "access" diff --git a/api/helper/builder/ocm_comparch.go b/api/helper/builder/ocm_comparch.go new file mode 100644 index 000000000..45b14789c --- /dev/null +++ b/api/helper/builder/ocm_comparch.go @@ -0,0 +1,20 @@ +package builder + +import ( + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/extensions/repositories/comparch" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +const T_COMPARCH = "component archive" + +func (b *Builder) ComponentArchive(path string, fmt accessio.FileFormat, name, vers string, f ...func()) { + r, err := comparch.Open(b.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, path, 0o777, accessio.PathFileSystem(b.FileSystem())) + b.failOn(err) + r.SetName(name) + r.SetVersion(vers) + r.GetDescriptor().Provider.Name = metav1.ProviderName("ACME") + + b.configure(&ocmVersion{ComponentVersionAccess: r, kind: T_COMPARCH}, f) +} diff --git a/pkg/env/builder/ocm_component.go b/api/helper/builder/ocm_component.go similarity index 90% rename from pkg/env/builder/ocm_component.go rename to api/helper/builder/ocm_component.go index d2565e611..0782f1205 100644 --- a/pkg/env/builder/ocm_component.go +++ b/api/helper/builder/ocm_component.go @@ -1,7 +1,7 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + "ocm.software/ocm/api/ocm/cpi" ) const T_OCMCOMPONENT = "component" diff --git a/pkg/env/builder/ocm_composition.go b/api/helper/builder/ocm_composition.go similarity index 77% rename from pkg/env/builder/ocm_composition.go rename to api/helper/builder/ocm_composition.go index db4571cda..a63656232 100644 --- a/pkg/env/builder/ocm_composition.go +++ b/api/helper/builder/ocm_composition.go @@ -1,7 +1,7 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/composition" + "ocm.software/ocm/api/ocm/extensions/repositories/composition" ) const T_OCM_COMPOSITION = "ocm composition repositoryt" diff --git a/api/helper/builder/ocm_ctf.go b/api/helper/builder/ocm_ctf.go new file mode 100644 index 000000000..6bcf62473 --- /dev/null +++ b/api/helper/builder/ocm_ctf.go @@ -0,0 +1,15 @@ +package builder + +import ( + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +const T_OCM_CTF = "ocm common transport format" + +func (b *Builder) OCMCommonTransport(path string, fmt accessio.FileFormat, f ...func()) { + r, err := ctf.Open(b.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, path, 0o777, fmt, accessio.PathFileSystem(b.FileSystem())) + b.failOn(err) + b.configure(&ocmRepository{Repository: r, kind: T_OCM_CTF}, f) +} diff --git a/pkg/env/builder/ocm_identity.go b/api/helper/builder/ocm_identity.go similarity index 100% rename from pkg/env/builder/ocm_identity.go rename to api/helper/builder/ocm_identity.go diff --git a/pkg/env/builder/ocm_label.go b/api/helper/builder/ocm_label.go similarity index 89% rename from pkg/env/builder/ocm_label.go rename to api/helper/builder/ocm_label.go index ff9982b8b..95feed032 100644 --- a/pkg/env/builder/ocm_label.go +++ b/api/helper/builder/ocm_label.go @@ -1,7 +1,7 @@ package builder import ( - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" ) const T_OCMLABELS = "element with labels" diff --git a/pkg/env/builder/ocm_provider.go b/api/helper/builder/ocm_provider.go similarity index 81% rename from pkg/env/builder/ocm_provider.go rename to api/helper/builder/ocm_provider.go index 190052c5c..8a6f2a32a 100644 --- a/pkg/env/builder/ocm_provider.go +++ b/api/helper/builder/ocm_provider.go @@ -1,8 +1,8 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" ) type ocmProvider struct { diff --git a/pkg/env/builder/ocm_reference.go b/api/helper/builder/ocm_reference.go similarity index 91% rename from pkg/env/builder/ocm_reference.go rename to api/helper/builder/ocm_reference.go index b9b84fa6a..c6a43ad83 100644 --- a/pkg/env/builder/ocm_reference.go +++ b/api/helper/builder/ocm_reference.go @@ -1,7 +1,7 @@ package builder import ( - "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" + "ocm.software/ocm/api/ocm/compdesc" ) type ocmReference struct { diff --git a/api/helper/builder/ocm_repo.go b/api/helper/builder/ocm_repo.go new file mode 100644 index 000000000..35900e2e7 --- /dev/null +++ b/api/helper/builder/ocm_repo.go @@ -0,0 +1,41 @@ +package builder + +import ( + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg" + "ocm.software/ocm/api/ocm/extensions/repositories/ocireg" + ocm "ocm.software/ocm/api/ocm/types" +) + +const T_OCMREPOSITORY = "ocm repository" + +type ocmRepository struct { + base + kind string + cpi.Repository +} + +func (r *ocmRepository) Type() string { + if r.kind != "" { + return r.kind + } + return T_OCMREPOSITORY +} + +func (r *ocmRepository) Set() { + r.Builder.ocm_repo = r.Repository + r.Builder.oci_repo = genericocireg.GetOCIRepository(r.Repository) +} + +func (b *Builder) OCMRepository(spec ocm.RepositorySpec, f ...func()) { + repo, err := b.OCMContext().RepositoryForSpec(spec) + b.failOn(err) + b.configure(&ocmRepository{Repository: repo, kind: T_OCMREPOSITORY}, f) +} + +func (b *Builder) OCIBasedOCMRepository(url string, path string, f ...func()) { + spec := ocireg.NewRepositorySpec(url, &ocireg.ComponentRepositoryMeta{ + SubPath: path, + }) + b.OCMRepository(spec, f...) +} diff --git a/pkg/env/builder/ocm_resource.go b/api/helper/builder/ocm_resource.go similarity index 89% rename from pkg/env/builder/ocm_resource.go rename to api/helper/builder/ocm_resource.go index 44d352857..9b3ffe30d 100644 --- a/pkg/env/builder/ocm_resource.go +++ b/api/helper/builder/ocm_resource.go @@ -3,10 +3,10 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/contexts/ocm" - "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/compdesc" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type ocmResource struct { diff --git a/pkg/env/builder/ocm_source.go b/api/helper/builder/ocm_source.go similarity index 88% rename from pkg/env/builder/ocm_source.go rename to api/helper/builder/ocm_source.go index d23f41f28..da39ec580 100644 --- a/pkg/env/builder/ocm_source.go +++ b/api/helper/builder/ocm_source.go @@ -3,8 +3,8 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" + "ocm.software/ocm/api/ocm/compdesc" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type ocmSource struct { diff --git a/pkg/env/builder/ocm_version.go b/api/helper/builder/ocm_version.go similarity index 90% rename from pkg/env/builder/ocm_version.go rename to api/helper/builder/ocm_version.go index 204d202da..6c0939e56 100644 --- a/pkg/env/builder/ocm_version.go +++ b/api/helper/builder/ocm_version.go @@ -3,8 +3,8 @@ package builder import ( "github.com/mandelsoft/goutils/errors" - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" - "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/cpi" ) const T_OCMVERSION = "component version" diff --git a/api/helper/builder/ocm_version_test.go b/api/helper/builder/ocm_version_test.go new file mode 100644 index 000000000..e1841cceb --- /dev/null +++ b/api/helper/builder/ocm_version_test.go @@ -0,0 +1,60 @@ +package builder_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/helper/builder" + . "ocm.software/ocm/api/ocm/testhelper" + + "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" + "ocm.software/ocm/api/ocm/extensions/attrs/compositionmodeattr" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + ocmutils "ocm.software/ocm/api/ocm/ocmutils" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" +) + +const ( + ARCH = "/tmp/ctf" + ARCH2 = "/tmp/ctf2" + PROVIDER = "open-component-model" + VERSION = "v1" + COMPONENT = "github.com/open-component-model/test" + OUT = "/tmp/res" +) + +var _ = Describe("Transfer handler", func() { + var env *Builder + + BeforeEach(func() { + env = NewBuilder() + compositionmodeattr.Set(env.OCMContext(), true) + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMPONENT, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + TestDataResource(env) + }) + }) + }) + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("add ocm resource", func() { + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env)) + cv := Must(src.LookupComponentVersion(COMPONENT, VERSION)) + + Expect(len(cv.GetDescriptor().Resources)).To(Equal(1)) + + r := Must(cv.GetResourceByIndex(0)) + a := Must(r.Access()) + Expect(a.GetType()).To(Equal(localblob.Type)) + + data := Must(ocmutils.GetResourceData(r)) + Expect(string(data)).To(Equal(S_TESTDATA)) + }) +}) diff --git a/pkg/env/builder/rsa_keypair.go b/api/helper/builder/rsa_keypair.go similarity index 82% rename from pkg/env/builder/rsa_keypair.go rename to api/helper/builder/rsa_keypair.go index 10b4452ed..8daf44956 100644 --- a/pkg/env/builder/rsa_keypair.go +++ b/api/helper/builder/rsa_keypair.go @@ -3,10 +3,10 @@ package builder import ( "github.com/mandelsoft/filepath/pkg/filepath" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/signingattr" - "github.com/open-component-model/ocm/pkg/signing/handlers/rsa" - "github.com/open-component-model/ocm/pkg/signing/signutils" - "github.com/open-component-model/ocm/pkg/utils" + "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" + "ocm.software/ocm/api/tech/signing/handlers/rsa" + "ocm.software/ocm/api/tech/signing/signutils" + "ocm.software/ocm/api/utils" ) // TODO: switch to context local setting. diff --git a/pkg/env/builder/suite_test.go b/api/helper/builder/suite_test.go similarity index 100% rename from pkg/env/builder/suite_test.go rename to api/helper/builder/suite_test.go diff --git a/pkg/env/env.go b/api/helper/env/env.go similarity index 96% rename from pkg/env/env.go rename to api/helper/env/env.go index ee9d08340..b3c51b7b3 100644 --- a/pkg/env/env.go +++ b/api/helper/env/env.go @@ -20,13 +20,13 @@ import ( "github.com/mandelsoft/vfs/pkg/readonlyfs" "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/open-component-model/ocm/pkg/common/accessio" - "github.com/open-component-model/ocm/pkg/contexts/config" - "github.com/open-component-model/ocm/pkg/contexts/credentials" - "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" - "github.com/open-component-model/ocm/pkg/contexts/oci" - ocm "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" - "github.com/open-component-model/ocm/pkg/utils" + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/oci" + ocm "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/accessio" ) //////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/env/env_test.go b/api/helper/env/env_test.go similarity index 91% rename from pkg/env/env_test.go rename to api/helper/env/env_test.go index ab7ae3b64..f225832e3 100644 --- a/pkg/env/env_test.go +++ b/api/helper/env/env_test.go @@ -7,7 +7,7 @@ import ( "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/open-component-model/ocm/pkg/contexts/ocm" + "ocm.software/ocm/api/ocm" ) var _ = Describe("Environment", func() { diff --git a/pkg/env/keypair.go b/api/helper/env/keypair.go similarity index 81% rename from pkg/env/keypair.go rename to api/helper/env/keypair.go index f1b286b7b..a9139b95b 100644 --- a/pkg/env/keypair.go +++ b/api/helper/env/keypair.go @@ -3,10 +3,10 @@ package env import ( "github.com/mandelsoft/filepath/pkg/filepath" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/signingattr" - "github.com/open-component-model/ocm/pkg/signing/handlers/rsa" - "github.com/open-component-model/ocm/pkg/signing/signutils" - "github.com/open-component-model/ocm/pkg/utils" + "ocm.software/ocm/api/ocm/extensions/attrs/signingattr" + "ocm.software/ocm/api/tech/signing/handlers/rsa" + "ocm.software/ocm/api/tech/signing/signutils" + "ocm.software/ocm/api/utils" ) func (e *Environment) RSAKeyPair(name ...string) { diff --git a/pkg/env/suite_test.go b/api/helper/env/suite_test.go similarity index 100% rename from pkg/env/suite_test.go rename to api/helper/env/suite_test.go diff --git a/pkg/env/testdata/testfile.txt b/api/helper/env/testdata/testfile.txt similarity index 100% rename from pkg/env/testdata/testfile.txt rename to api/helper/env/testdata/testfile.txt diff --git a/api/oci/action_test.go b/api/oci/action_test.go new file mode 100644 index 000000000..03d5c1407 --- /dev/null +++ b/api/oci/action_test.go @@ -0,0 +1,18 @@ +package oci_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/oci" + oci_repository_prepare "ocm.software/ocm/api/oci/extensions/actions/oci-repository-prepare" +) + +var _ = Describe("action registration", func() { + It("registers oci prepare", func() { + a := oci.DefaultContext().GetActions().GetActionTypes().GetAction(oci_repository_prepare.Type) + Expect(a).NotTo(BeNil()) + v := a.GetVersion("v1") + Expect(v).NotTo(BeNil()) + }) +}) diff --git a/pkg/contexts/oci/annotations/annotations.go b/api/oci/annotations/annotations.go similarity index 100% rename from pkg/contexts/oci/annotations/annotations.go rename to api/oci/annotations/annotations.go diff --git a/api/oci/art_test.go b/api/oci/art_test.go new file mode 100644 index 000000000..d118fb22a --- /dev/null +++ b/api/oci/art_test.go @@ -0,0 +1,38 @@ +package oci_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opencontainers/go-digest" + + "ocm.software/ocm/api/oci" +) + +func CheckArt(ref string, exp *oci.ArtSpec) { + spec, err := oci.ParseArt(ref) + if exp == nil { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).To(Succeed()) + Expect(spec).To(Equal(*exp)) + } +} + +var _ = Describe("art parsing", func() { + digest := digest.Digest("sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a") + tag := "v1" + + It("succeeds", func() { + CheckArt("ubuntu", &oci.ArtSpec{Repository: "ubuntu"}) + CheckArt("ubuntu/test", &oci.ArtSpec{Repository: "ubuntu/test"}) + CheckArt("ubuntu/test@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", Digest: &digest}) + CheckArt("ubuntu/test:"+tag, &oci.ArtSpec{Repository: "ubuntu/test", Tag: &tag}) + CheckArt("ubuntu/test:"+tag+"@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", Digest: &digest, Tag: &tag}) + }) + + It("fails", func() { + CheckArt("ubu@ntu", nil) + CheckArt("ubu@sha256:123", nil) + }) +}) diff --git a/api/oci/artdesc/artifact.go b/api/oci/artdesc/artifact.go new file mode 100644 index 000000000..f626c93d1 --- /dev/null +++ b/api/oci/artdesc/artifact.go @@ -0,0 +1,298 @@ +package artdesc + +import ( + "encoding/json" + out "fmt" + + "github.com/containerd/containerd/images" + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "ocm.software/ocm/api/oci/artdesc/helper" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +const SchemeVersion = helper.SchemeVersion + +const ( + MediaTypeImageManifest = ociv1.MediaTypeImageManifest + MediaTypeImageIndex = ociv1.MediaTypeImageIndex + MediaTypeImageLayer = ociv1.MediaTypeImageLayer + MediaTypeImageLayerGzip = ociv1.MediaTypeImageLayerGzip + + MediaTypeDockerSchema2Manifest = images.MediaTypeDockerSchema2Manifest + MediaTypeDockerSchema2ManifestList = images.MediaTypeDockerSchema2ManifestList + + MediaTypeImageConfig = ociv1.MediaTypeImageConfig +) + +var legacy = false + +type ( + Descriptor = ociv1.Descriptor + Platform = ociv1.Platform +) + +type ArtifactDescriptor interface { + IsManifest() bool + IsIndex() bool + IsValid() bool + + Digest() digest.Digest + Blob() (blobaccess.BlobAccess, error) + Artifact() *Artifact + Manifest() (*Manifest, error) + Index() (*Index, error) +} + +type BlobDescriptorSource interface { + GetBlobDescriptor(digest.Digest) *Descriptor + MimeType() string + IsValid() bool +} + +// Artifact is the unified representation of an OCI artifact +// according to https://github.com/opencontainers/image-spec/blob/main/manifest.md +// It is either an image manifest or an image index manifest (fat image). +type Artifact struct { + manifest *Manifest + index *Index +} + +var ( + _ ArtifactDescriptor = (*Artifact)(nil) + _ BlobDescriptorSource = (*Artifact)(nil) + _ json.Marshaler = (*Artifact)(nil) + _ json.Unmarshaler = (*Artifact)(nil) +) + +func New() *Artifact { + return &Artifact{} +} + +func NewManifestArtifact() *Artifact { + a := New() + a.SetManifest(NewManifest()) + return a +} + +func NewIndexArtifact() *Artifact { + a := New() + a.SetIndex(NewIndex()) + return a +} + +func (d *Artifact) Digest() digest.Digest { + var blob blobaccess.BlobAccess + if d.manifest != nil { + blob, _ = d.manifest.Blob() + } + if d.index != nil { + blob, _ = d.index.Blob() + } + if blob != nil { + return blob.Digest() + } + return "" +} + +func (d *Artifact) Blob() (blobaccess.BlobAccess, error) { + if d.manifest != nil { + return d.manifest.Blob() + } + if d.index != nil { + return d.index.Blob() + } + return nil, errors.ErrInvalid("oci artifact") +} + +func (d *Artifact) Artifact() *Artifact { + return d +} + +func (d *Artifact) MimeType() string { + if d.IsIndex() { + return d.index.MimeType() + } + if d.IsManifest() { + return d.manifest.MimeType() + } + return "" +} + +func (d *Artifact) SetManifest(m *Manifest) error { + if d.IsIndex() || d.IsManifest() { + return errors.Newf("artifact descriptor already instantiated") + } + d.manifest = m + return nil +} + +func (d *Artifact) SetIndex(i *Index) error { + if d.IsIndex() || d.IsManifest() { + return errors.Newf("artifact descriptor already instantiated") + } + d.index = i + return nil +} + +func (d *Artifact) IsValid() bool { + return d.manifest != nil || d.index != nil +} + +func (d *Artifact) IsManifest() bool { + return d.manifest != nil +} + +func (d *Artifact) IsIndex() bool { + return d.index != nil +} + +func (d *Artifact) Index() (*Index, error) { + if d.index != nil { + return d.index, nil + } + return nil, errors.ErrInvalid() +} + +func (d *Artifact) Manifest() (*Manifest, error) { + if d.manifest != nil { + return d.manifest, nil + } + return nil, errors.ErrInvalid() +} + +func (d *Artifact) SetAnnotation(name, value string) error { + return d.modifyAnnotation(func(annos *map[string]string) { + if *annos == nil { + *annos = map[string]string{} + } + (*annos)[name] = value + }) +} + +func (d *Artifact) GetAnnotation(name string) string { + var annos map[string]string + switch { + case d.manifest != nil: + annos = d.manifest.Annotations + case d.index != nil: + annos = d.index.Annotations + default: + return "" + } + if len(annos) == 0 { + return "" + } + return annos[name] +} + +func (d *Artifact) DeleteAnnotation(name string) error { + return d.modifyAnnotation(func(annos *map[string]string) { + if *annos == nil { + return + } + delete(*annos, name) + if len(*annos) == 0 { + *annos = nil + } + }) +} + +func (d *Artifact) modifyAnnotation(mod func(annos *map[string]string)) error { + var annos map[string]string + + switch { + case d.manifest != nil: + annos = d.manifest.Annotations + case d.index != nil: + annos = d.index.Annotations + default: + return errors.Newf("void artifact access") + } + mod(&annos) + if d.manifest != nil { + d.manifest.Annotations = annos + } else { + d.index.Annotations = annos + } + return nil +} + +func (d *Artifact) ToBlobAccess() (blobaccess.BlobAccess, error) { + if d.IsManifest() { + return d.manifest.Blob() + } + if d.IsIndex() { + return d.index.Blob() + } + return nil, errors.ErrInvalid("artifact descriptor") +} + +func (d *Artifact) GetBlobDescriptor(digest digest.Digest) *Descriptor { + if d.IsManifest() { + m, err := d.Manifest() + if err != nil { + out.Printf("manifest was empty for artifact digest %s", digest) + + return nil + } + return m.GetBlobDescriptor(digest) + } + if d.IsIndex() { + i, _ := d.Index() + return i.GetBlobDescriptor(digest) + } + return nil +} + +func (d Artifact) MarshalJSON() ([]byte, error) { + if d.manifest != nil { + d.manifest.MediaType = ArtifactMimeType(d.manifest.MediaType, ociv1.MediaTypeImageManifest, legacy) + return json.Marshal(d.manifest) + } + if d.index != nil { + d.index.MediaType = ArtifactMimeType(d.index.MediaType, ociv1.MediaTypeImageIndex, legacy) + return json.Marshal(d.index) + } + return []byte("null"), nil +} + +func (d *Artifact) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + return nil + } + var m helper.GenericDescriptor + + err := json.Unmarshal(data, &m) + if err != nil { + return err + } + + err = m.Validate() + if err != nil { + return err + } + if m.IsManifest() { + d.manifest = (*Manifest)(m.AsManifest()) + d.index = nil + } else { + d.index = (*Index)(m.AsIndex()) + d.manifest = nil + } + return nil +} + +func Decode(data []byte) (*Artifact, error) { + var d Artifact + + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil +} + +func Encode(d *Artifact) ([]byte, error) { + return json.Marshal(d) +} diff --git a/api/oci/artdesc/config.go b/api/oci/artdesc/config.go new file mode 100644 index 000000000..ccfc5df1b --- /dev/null +++ b/api/oci/artdesc/config.go @@ -0,0 +1,25 @@ +package artdesc + +import ( + "encoding/json" + + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +type ImageConfig = ociv1.Image + +func ParseImageConfig(blob blobaccess.BlobAccess) (*ImageConfig, error) { + var cfg ImageConfig + + data, err := blob.Get() + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &cfg) + if err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/pkg/contexts/oci/artdesc/helper/generic.go b/api/oci/artdesc/helper/generic.go similarity index 100% rename from pkg/contexts/oci/artdesc/helper/generic.go rename to api/oci/artdesc/helper/generic.go diff --git a/api/oci/artdesc/index.go b/api/oci/artdesc/index.go new file mode 100644 index 000000000..fe741236a --- /dev/null +++ b/api/oci/artdesc/index.go @@ -0,0 +1,117 @@ +package artdesc + +import ( + "encoding/json" + + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +type Index ociv1.Index + +var _ BlobDescriptorSource = (*Index)(nil) + +func NewIndex() *Index { + return &Index{ + Versioned: specs.Versioned{SchemeVersion}, + MediaType: MediaTypeImageIndex, + Manifests: nil, + Annotations: nil, + } +} + +var _ ArtifactDescriptor = (*Index)(nil) + +func (i *Index) IsManifest() bool { + return false +} + +func (i *Index) IsIndex() bool { + return true +} + +func (i *Index) Digest() digest.Digest { + blob, _ := i.Blob() + if blob != nil { + return blob.Digest() + } + return "" +} + +func (i *Index) Artifact() *Artifact { + return &Artifact{index: i} +} + +func (i *Index) Manifest() (*Manifest, error) { + return nil, errors.ErrInvalid() +} + +func (i *Index) Index() (*Index, error) { + return i, nil +} + +func (i *Index) IsValid() bool { + return true +} + +func (i *Index) GetBlobDescriptor(digest digest.Digest) *Descriptor { + for _, m := range i.Manifests { + if m.Digest == digest { + return &m + } + } + return nil +} + +func (i *Index) MimeType() string { + return ArtifactMimeType(i.MediaType, MediaTypeImageIndex, legacy) +} + +func (i *Index) SetAnnotation(name, value string) { + if i.Annotations == nil { + i.Annotations = map[string]string{} + } + i.Annotations[name] = value +} + +func (i *Index) DeleteAnnotation(name string) { + if i.Annotations == nil { + return + } + delete(i.Annotations, name) + if len(i.Annotations) == 0 { + i.Annotations = nil + } +} + +func (i *Index) Blob() (blobaccess.BlobAccess, error) { + i.MediaType = i.MimeType() + data, err := json.Marshal(i) + if err != nil { + return nil, err + } + return blobaccess.ForData(i.MediaType, data), nil +} + +func (i *Index) AddManifest(d *Descriptor) { + i.Manifests = append(i.Manifests, *d) +} + +//////////////////////////////////////////////////////////////////////////////// + +func DecodeIndex(data []byte) (*Index, error) { + var d Index + + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil +} + +func EncodeIndex(d *Index) ([]byte, error) { + return json.Marshal(d) +} diff --git a/pkg/contexts/oci/artdesc/manifest.go b/api/oci/artdesc/manifest.go similarity index 97% rename from pkg/contexts/oci/artdesc/manifest.go rename to api/oci/artdesc/manifest.go index ed8ae2247..c0f990903 100644 --- a/pkg/contexts/oci/artdesc/manifest.go +++ b/api/oci/artdesc/manifest.go @@ -8,7 +8,7 @@ import ( "github.com/opencontainers/image-spec/specs-go" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type Manifest ociv1.Manifest diff --git a/pkg/contexts/oci/artdesc/suite_test.go b/api/oci/artdesc/suite_test.go similarity index 100% rename from pkg/contexts/oci/artdesc/suite_test.go rename to api/oci/artdesc/suite_test.go diff --git a/api/oci/artdesc/utils.go b/api/oci/artdesc/utils.go new file mode 100644 index 000000000..1ac3126e2 --- /dev/null +++ b/api/oci/artdesc/utils.go @@ -0,0 +1,132 @@ +package artdesc + +import ( + "strings" + + "github.com/opencontainers/go-digest" + + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +func DefaultBlobDescriptor(blob blobaccess.BlobAccess) *Descriptor { + return &Descriptor{ + MediaType: blob.MimeType(), + Digest: blob.Digest(), + Size: blob.Size(), + URLs: nil, + Annotations: nil, + Platform: nil, + } +} + +func IsDigest(version string) (bool, digest.Digest) { + if strings.HasPrefix(version, "@") { + return true, digest.Digest(version[1:]) + } + if strings.Contains(version, ":") { + return true, digest.Digest(version) + } + return false, "" +} + +func ToContentMediaType(media string) string { +loop: + for { + last := strings.LastIndex(media, "+") + if last < 0 { + break + } + switch media[last+1:] { + case "tar": + fallthrough + case "gzip": + fallthrough + case "yaml": + fallthrough + case "json": + media = media[:last] + default: + break loop + } + } + return media +} + +func ToDescriptorMediaType(media string) string { + return ToContentMediaType(media) + "+json" +} + +func ToArchiveMediaTypes(media string) []string { + base := ToContentMediaType(media) + return []string{base + "+tar", base + "+tar+gzip"} +} + +func IsOCIMediaType(media string) bool { + c := ToContentMediaType(media) + for _, t := range ContentTypes() { + if t == c { + return true + } + } + return false +} + +func ContentTypes() []string { + r := []string{} + for _, t := range DescriptorTypes() { + r = append(r, ToContentMediaType(t)) + } + return r +} + +func DescriptorTypes() []string { + return []string{ + MediaTypeImageManifest, + MediaTypeImageIndex, + MediaTypeDockerSchema2Manifest, + MediaTypeDockerSchema2ManifestList, + } +} + +func ArchiveBlobTypes() []string { + r := []string{} + for _, t := range ContentTypes() { + r = append(r, ToArchiveMediaTypes(t)...) + } + return r +} + +func ArtifactMimeType(cur, def string, legacy bool) string { + if cur != "" { + return cur + } + return MapArtifactMimeType(def, legacy) +} + +func MapArtifactMimeType(mime string, legacy bool) string { + if legacy { + switch mime { + case MediaTypeImageManifest: + return MediaTypeDockerSchema2Manifest + case MediaTypeImageIndex: + return MediaTypeDockerSchema2ManifestList + } + } else { + switch mime { + case MediaTypeDockerSchema2Manifest: + // return MediaTypeImageManifest + case MediaTypeDockerSchema2ManifestList: + // return MediaTypeImageIndex + } + } + return mime +} + +func MapArtifactBlobMimeType(blob blobaccess.BlobAccess, legacy bool) blobaccess.BlobAccess { + mime := blob.MimeType() + mapped := MapArtifactMimeType(mime, legacy) + if mapped != mime { + return blobaccess.WithMimeType(mapped, blob) + } + return blob +} diff --git a/api/oci/artdesc/utils_test.go b/api/oci/artdesc/utils_test.go new file mode 100644 index 000000000..9683784aa --- /dev/null +++ b/api/oci/artdesc/utils_test.go @@ -0,0 +1,20 @@ +package artdesc_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/oci/artdesc" +) + +var _ = Describe("utils", func() { + It("strips media type", func() { + Expect(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)).To(Equal("application/vnd.oci.image.manifest.v1")) + Expect(artdesc.ToContentMediaType(artdesc.MediaTypeImageIndex)).To(Equal("application/vnd.oci.image.index.v1")) + }) + + It("maps to descriptor media typ", func() { + Expect(artdesc.ToDescriptorMediaType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest) + "+tar+gzip")).To(Equal(artdesc.MediaTypeImageManifest)) + Expect(artdesc.ToDescriptorMediaType(artdesc.ToContentMediaType(artdesc.MediaTypeImageIndex) + "+tar+gzip")).To(Equal(artdesc.MediaTypeImageIndex)) + }) +}) diff --git a/api/oci/builder.go b/api/oci/builder.go new file mode 100644 index 000000000..fea508f64 --- /dev/null +++ b/api/oci/builder.go @@ -0,0 +1,29 @@ +package oci + +import ( + "context" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/oci/internal" +) + +func WithContext(ctx context.Context) internal.Builder { + return internal.Builder{}.WithContext(ctx) +} + +func WithCredentials(ctx credentials.Context) internal.Builder { + return internal.Builder{}.WithCredentials(ctx) +} + +func WithRepositoyTypeScheme(scheme RepositoryTypeScheme) internal.Builder { + return internal.Builder{}.WithRepositoyTypeScheme(scheme) +} + +func WithRepositorySpecHandlers(reg RepositorySpecHandlers) internal.Builder { + return internal.Builder{}.WithRepositorySpecHandlers(reg) +} + +func New(mode ...datacontext.BuilderMode) Context { + return internal.Builder{}.New(mode...) +} diff --git a/api/oci/config/config_test.go b/api/oci/config/config_test.go new file mode 100644 index 000000000..f95f29320 --- /dev/null +++ b/api/oci/config/config_test.go @@ -0,0 +1,79 @@ +package config_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/oci/config" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/extensions/repositories/ocireg" +) + +func normalize(i interface{}) ([]byte, error) { + data, err := json.Marshal(i) + if err != nil { + return nil, err + } + var generic map[string]interface{} + err = json.Unmarshal(data, &generic) + if err != nil { + return nil, err + } + return json.Marshal(generic) +} + +var _ = Describe("oci config", func() { + spec := ocireg.NewRepositorySpec("ghcr.io") + data, err := normalize(spec) + Expect(err).To(Succeed()) + + specdata := "{\"aliases\":{\"alias\":" + string(data) + "},\"type\":\"" + config.ConfigType + "\"}" + + Context("serialize", func() { + It("serializes config", func() { + cfg := config.New() + err := cfg.SetAlias("alias", spec) + Expect(err).To(Succeed()) + + data, err := normalize(cfg) + + Expect(err).To(Succeed()) + Expect(data).To(Equal([]byte(specdata))) + + cfg2 := config.New() + err = json.Unmarshal(data, cfg2) + Expect(err).To(Succeed()) + Expect(cfg2).To(Equal(cfg)) + }) + }) + + Context("apply", func() { + It("applies directly", func() { + ctx := cpi.New() + + cfg := config.New() + err := cfg.SetAlias("alias", spec) + Expect(err).To(Succeed()) + + Expect(cfg.ApplyTo(ctx.ConfigContext(), ctx)).To(Succeed()) + + found := ctx.GetAlias("alias") + Expect(found).To(Equal(cfg.Aliases["alias"])) + }) + + It("applies via config context", func() { + ctx := cpi.New() + + cfg := config.New() + err := cfg.SetAlias("alias", spec) + Expect(err).To(Succeed()) + + Expect(ctx.ConfigContext().ApplyConfig(cfg, "programmatic")).To(Succeed()) + + found := ctx.GetAlias("alias") + Expect(found).To(Equal(cfg.Aliases["alias"])) + }) + }) +}) diff --git a/pkg/contexts/oci/config/suite_test.go b/api/oci/config/suite_test.go similarity index 100% rename from pkg/contexts/oci/config/suite_test.go rename to api/oci/config/suite_test.go diff --git a/api/oci/config/type.go b/api/oci/config/type.go new file mode 100644 index 000000000..2d532af63 --- /dev/null +++ b/api/oci/config/type.go @@ -0,0 +1,70 @@ +package config + +import ( + "ocm.software/ocm/api/config" + cfgcpi "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ConfigType = "oci" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX + ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1" +) + +func init() { + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage)) + cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage)) +} + +// Config describes a memory based config interface. +type Config struct { + runtime.ObjectVersionedType `json:",inline"` + Aliases map[string]*cpi.GenericRepositorySpec `json:"aliases,omitempty"` +} + +// New creates a new memory ConfigSpec. +func New() *Config { + return &Config{ + ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType), + } +} + +func (a *Config) GetType() string { + return ConfigType +} + +func (a *Config) SetAlias(name string, spec cpi.RepositorySpec) error { + g, err := cpi.ToGenericRepositorySpec(spec) + if err != nil { + return err + } + if a.Aliases == nil { + a.Aliases = map[string]*cpi.GenericRepositorySpec{} + } + a.Aliases[name] = g + return nil +} + +func (a *Config) ApplyTo(ctx config.Context, target interface{}) error { + t, ok := target.(cpi.Context) + if !ok { + return config.ErrNoContext(ConfigType) + } + for n, s := range a.Aliases { + t.SetAlias(n, s) + } + return nil +} + +const usage = ` +The config type
` + ConfigType + `
can be used to define
+OCI registry aliases:
+
++ type: ` + ConfigType + ` + aliases: + <name>: <OCI registry specification> + ... ++` diff --git a/pkg/contexts/oci/cpi/README.md b/api/oci/cpi/README.md similarity index 100% rename from pkg/contexts/oci/cpi/README.md rename to api/oci/cpi/README.md diff --git a/api/oci/cpi/interface.go b/api/oci/cpi/interface.go new file mode 100644 index 000000000..f74921792 --- /dev/null +++ b/api/oci/cpi/interface.go @@ -0,0 +1,95 @@ +package cpi + +// This is the Context Provider Interface for credential providers + +import ( + "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/oci/internal" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +const CONTEXT_TYPE = internal.CONTEXT_TYPE + +const CommonTransportFormat = internal.CommonTransportFormat + +type ( + Context = internal.Context + ContextProvider = internal.ContextProvider + Repository = internal.Repository + RepositorySpecHandlers = internal.RepositorySpecHandlers + RepositorySpecHandler = internal.RepositorySpecHandler + UniformRepositorySpec = internal.UniformRepositorySpec + RepositoryType = internal.RepositoryType + RepositoryTypeProvider = internal.RepositoryTypeProvider + RepositoryTypeScheme = internal.RepositoryTypeScheme + RepositorySpec = internal.RepositorySpec + IntermediateRepositorySpecAspect = internal.IntermediateRepositorySpecAspect + GenericRepositorySpec = internal.GenericRepositorySpec + ArtifactAccess = internal.ArtifactAccess + Artifact = internal.Artifact + ArtifactSource = internal.ArtifactSource + ArtifactSink = internal.ArtifactSink + BlobSource = internal.BlobSource + BlobSink = internal.BlobSink + NamespaceLister = internal.NamespaceLister + NamespaceAccess = internal.NamespaceAccess + ManifestAccess = internal.ManifestAccess + IndexAccess = internal.IndexAccess + BlobAccess = internal.BlobAccess + DataAccess = internal.DataAccess + RepositorySource = internal.RepositorySource + ConsumerIdentityProvider = internal.ConsumerIdentityProvider +) + +type Descriptor = ociv1.Descriptor + +func DefaultContext() Context { + return internal.DefaultContext +} + +func New(m ...datacontext.BuilderMode) Context { + return internal.Builder{}.New(m...) +} + +func FromProvider(p ContextProvider) Context { + return internal.FromProvider(p) +} + +func RegisterRepositorySpecHandler(handler RepositorySpecHandler, types ...string) { + internal.RegisterRepositorySpecHandler(handler, types...) +} + +func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error) { + return internal.ToGenericRepositorySpec(spec) +} + +func UniformRepositorySpecForHostURL(typ string, host string) *UniformRepositorySpec { + return internal.UniformRepositorySpecForHostURL(typ, host) +} + +const ( + KIND_OCIARTIFACT = internal.KIND_OCIARTIFACT + KIND_MEDIATYPE = blobaccess.KIND_MEDIATYPE + KIND_BLOB = blobaccess.KIND_BLOB +) + +func ErrUnknownArtifact(name, version string) error { + return internal.ErrUnknownArtifact(name, version) +} + +func ErrBlobNotFound(digest digest.Digest) error { + return blobaccess.ErrBlobNotFound(digest) +} + +func IsErrBlobNotFound(err error) bool { + return blobaccess.IsErrBlobNotFound(err) +} + +// provide context interface for other files to avoid diffs in imports. +var ( + newStrictRepositoryTypeScheme = internal.NewStrictRepositoryTypeScheme + defaultRepositoryTypeScheme = internal.DefaultRepositoryTypeScheme +) diff --git a/pkg/contexts/oci/cpi/mod.go b/api/oci/cpi/mod.go similarity index 89% rename from pkg/contexts/oci/cpi/mod.go rename to api/oci/cpi/mod.go index 7e4278156..21da6bb77 100644 --- a/pkg/contexts/oci/cpi/mod.go +++ b/api/oci/cpi/mod.go @@ -3,8 +3,8 @@ package cpi import ( "github.com/opencontainers/go-digest" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/utils/accessobj" ) type _Artifact = artdesc.Artifact diff --git a/api/oci/cpi/repotypes.go b/api/oci/cpi/repotypes.go new file mode 100644 index 000000000..f39e32b5f --- /dev/null +++ b/api/oci/cpi/repotypes.go @@ -0,0 +1,36 @@ +package cpi + +// this file is identical for contexts oci and credentials and similar for +// ocm. + +import ( + "ocm.software/ocm/api/utils/runtime" +) + +type RepositoryTypeVersionScheme = runtime.TypeVersionScheme[RepositorySpec, RepositoryType] + +func NewRepositoryTypeVersionScheme(kind string) RepositoryTypeVersionScheme { + return runtime.NewTypeVersionScheme[RepositorySpec, RepositoryType](kind, newStrictRepositoryTypeScheme()) +} + +func RegisterRepositoryType(rtype RepositoryType) { + defaultRepositoryTypeScheme.Register(rtype) +} + +func RegisterRepositoryTypeVersions(s RepositoryTypeVersionScheme) { + defaultRepositoryTypeScheme.AddKnownTypes(s) +} + +//////////////////////////////////////////////////////////////////////////////// + +func NewRepositoryType[I RepositorySpec](name string) RepositoryType { + return runtime.NewVersionedTypedObjectType[RepositorySpec, I](name) +} + +func NewRepositoryTypeByConverter[I RepositorySpec, V runtime.TypedObject](name string, converter runtime.Converter[I, V]) RepositoryType { + return runtime.NewVersionedTypedObjectTypeByConverter[RepositorySpec, I](name, converter) +} + +func NewRepositoryTypeByFormatVersion(name string, fmt runtime.FormatVersion[RepositorySpec]) RepositoryType { + return runtime.NewVersionedTypedObjectTypeByFormatVersion[RepositorySpec](name, fmt) +} diff --git a/api/oci/cpi/state.go b/api/oci/cpi/state.go new file mode 100644 index 000000000..53abaf18a --- /dev/null +++ b/api/oci/cpi/state.go @@ -0,0 +1,84 @@ +package cpi + +import ( + "reflect" + + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/utils/accessobj" +) + +type ManifestStateHandler struct{} + +var _ accessobj.StateHandler = &ManifestStateHandler{} + +func NewManifestStateHandler() accessobj.StateHandler { + return &ManifestStateHandler{} +} + +func (i ManifestStateHandler) Initial() interface{} { + return artdesc.NewManifest() +} + +func (i ManifestStateHandler) Encode(d interface{}) ([]byte, error) { + return artdesc.EncodeManifest(d.(*artdesc.Manifest)) +} + +func (i ManifestStateHandler) Decode(data []byte) (interface{}, error) { + return artdesc.DecodeManifest(data) +} + +func (i ManifestStateHandler) Equivalent(a, b interface{}) bool { + return reflect.DeepEqual(a, b) +} + +//////////////////////////////////////////////////////////////////////////////// + +type IndexStateHandler struct{} + +var _ accessobj.StateHandler = &IndexStateHandler{} + +func NewIndexStateHandler() accessobj.StateHandler { + return &IndexStateHandler{} +} + +func (i IndexStateHandler) Initial() interface{} { + return artdesc.NewIndex() +} + +func (i IndexStateHandler) Encode(d interface{}) ([]byte, error) { + return artdesc.EncodeIndex(d.(*artdesc.Index)) +} + +func (i IndexStateHandler) Decode(data []byte) (interface{}, error) { + return artdesc.DecodeIndex(data) +} + +func (i IndexStateHandler) Equivalent(a, b interface{}) bool { + return reflect.DeepEqual(a, b) +} + +//////////////////////////////////////////////////////////////////////////////// + +type ArtifactStateHandler struct{} + +var _ accessobj.StateHandler = &ArtifactStateHandler{} + +func NewArtifactStateHandler() accessobj.StateHandler { + return &ArtifactStateHandler{} +} + +func (i ArtifactStateHandler) Initial() interface{} { + return artdesc.New() +} + +func (i ArtifactStateHandler) Encode(d interface{}) ([]byte, error) { + return artdesc.Encode(d.(*artdesc.Artifact)) +} + +func (i ArtifactStateHandler) Decode(data []byte) (interface{}, error) { + return artdesc.Decode(data) +} + +func (i ArtifactStateHandler) Equivalent(a, b interface{}) bool { + return reflect.DeepEqual(a, b) +} diff --git a/pkg/contexts/oci/cpi/suite_test.go b/api/oci/cpi/suite_test.go similarity index 100% rename from pkg/contexts/oci/cpi/suite_test.go rename to api/oci/cpi/suite_test.go diff --git a/api/oci/cpi/support/artifact.go b/api/oci/cpi/support/artifact.go new file mode 100644 index 000000000..253348936 --- /dev/null +++ b/api/oci/cpi/support/artifact.go @@ -0,0 +1,271 @@ +package support + +import ( + "compress/gzip" + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/internal" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +var ErrNoIndex = errors.New("manifest does not support access to subsequent artifacts") + +type ArtifactAccessImpl struct { + cpi.ArtifactAccessImplBase + artifactBase +} + +var _ cpi.ArtifactAccessImpl = (*ArtifactAccessImpl)(nil) + +func NewArtifactForBlob(container NamespaceAccessImpl, blob blobaccess.BlobAccess, closer ...io.Closer) (cpi.ArtifactAccess, error) { + mode := accessobj.ACC_WRITABLE + if container.IsReadOnly() { + mode = accessobj.ACC_READONLY + } + state, err := accessobj.NewBlobStateForBlob(mode, blob, cpi.NewArtifactStateHandler()) + if err != nil { + return nil, err + } + return newArtifact(container, state, closer...) +} + +func NewArtifact(container NamespaceAccessImpl, defs ...cpi.Artifact) (cpi.ArtifactAccess, error) { + var def cpi.Artifact + if len(defs) != 0 && defs[0] != nil && defs[0].IsValid() { + def = defs[0].Artifact() + } + mode := accessobj.ACC_WRITABLE + if container.IsReadOnly() { + mode = accessobj.ACC_READONLY + } + state, err := accessobj.NewBlobStateForObject(mode, def, cpi.NewArtifactStateHandler()) + if err != nil { + return nil, fmt.Errorf("failed to fetch new blob state: %w", err) + } + return newArtifact(container, state) +} + +func newArtifact(container NamespaceAccessImpl, state accessobj.State, closer ...io.Closer) (cpi.ArtifactAccess, error) { + base, err := cpi.NewArtifactAccessImplBase(container, closer...) + if err != nil { + return nil, err + } + impl := &ArtifactAccessImpl{ + ArtifactAccessImplBase: *base, + artifactBase: newArtifactBase(container, state), + } + return cpi.NewArtifactAccess(impl), nil +} + +func (a *ArtifactAccessImpl) AddBlob(access cpi.BlobAccess) error { + return a.container.AddBlob(access) +} + +func (a *ArtifactAccessImpl) NewArtifact(art ...cpi.Artifact) (cpi.ArtifactAccess, error) { + if !a.IsIndex() { + return nil, ErrNoIndex + } + if a.IsReadOnly() { + return nil, accessio.ErrReadOnly + } + return NewArtifact(a.container, art...) +} + +//////////////////////////////////////////////////////////////////////////////// + +func (a *ArtifactAccessImpl) Artifact() *artdesc.Artifact { + return a.GetDescriptor() +} + +func (a *ArtifactAccessImpl) GetDescriptor() *artdesc.Artifact { + d := a.state.GetState().(*artdesc.Artifact) + if d.IsValid() { + return d + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// from artdesc.Artifact + +func (a *ArtifactAccessImpl) GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor { + d := a.GetDescriptor().GetBlobDescriptor(digest) + /* + if d == nil { + d = a.container.GetBlobDescriptor(digest) + } + */ + return d +} + +func (a *ArtifactAccessImpl) Index() (*artdesc.Index, error) { + a.lock.Lock() + defer a.lock.Unlock() + d, ok := a.state.GetState().(*artdesc.Artifact) + if !ok { + return nil, fmt.Errorf("failed to assert type %T to *artdesc.Artifact", a.state.GetState()) + } + if !d.IsValid() { + idx := artdesc.NewIndex() + if err := d.SetIndex(idx); err != nil { + return nil, errors.Newf("artifact is manifest") + } + } + return d.Index() +} + +func (a *ArtifactAccessImpl) Manifest() (*artdesc.Manifest, error) { + a.lock.Lock() + defer a.lock.Unlock() + d := a.state.GetState().(*artdesc.Artifact) + if !d.IsValid() { + m := artdesc.NewManifest() + if err := d.SetManifest(m); err != nil { + return nil, errors.Newf("artifact is index") + } + } + return d.Manifest() +} + +func (a *ArtifactAccessImpl) ManifestAccess(v cpi.ArtifactAccess) internal.ManifestAccess { + a.lock.Lock() + defer a.lock.Unlock() + d := a.state.GetState().(*artdesc.Artifact) + if !d.IsManifest() { + m := artdesc.NewManifest() + if err := d.SetManifest(m); err != nil { + return nil + } + } + return NewManifestForArtifact(v, a) +} + +func (a *ArtifactAccessImpl) IndexAccess(v cpi.ArtifactAccess) internal.IndexAccess { + a.lock.Lock() + defer a.lock.Unlock() + d := a.state.GetState().(*artdesc.Artifact) + if !d.IsIndex() { + i := artdesc.NewIndex() + if err := d.SetIndex(i); err != nil { + return nil + } + } + return NewIndexForArtifact(v, a) +} + +func (a *ArtifactAccessImpl) GetArtifact(digest digest.Digest) (cpi.ArtifactAccess, error) { + if !a.IsIndex() { + return nil, ErrNoIndex + } + return a.container.GetArtifact("@" + digest.String()) +} + +func (a *ArtifactAccessImpl) GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) { + return a.container.GetBlobData(digest) +} + +func (a *ArtifactAccessImpl) GetBlob(digest digest.Digest) (cpi.BlobAccess, error) { + d := a.GetBlobDescriptor(digest) + if d != nil { + size, data, err := a.container.GetBlobData(digest) + if err != nil { + return nil, err + } + err = AdjustSize(d, size) + if err != nil { + return nil, err + } + return blobaccess.ForDataAccess(d.Digest, d.Size, d.MediaType, data), nil + } + return nil, cpi.ErrBlobNotFound(digest) +} + +func (a *ArtifactAccessImpl) AddArtifact(art cpi.Artifact, platform *artdesc.Platform) (cpi.BlobAccess, error) { + if a.IsReadOnly() { + return nil, accessio.ErrReadOnly + } + d, err := a.Index() + if err != nil { + return nil, err + } + + a.lock.Lock() + defer a.lock.Unlock() + + blob, err := a.container.AddArtifact(art) + if err != nil { + return nil, err + } + d.Manifests = append(d.Manifests, cpi.Descriptor{ + MediaType: blob.MimeType(), + Digest: blob.Digest(), + Size: blob.Size(), + URLs: nil, + Annotations: nil, + Platform: platform, + }) + return blob, nil +} + +func (a *ArtifactAccessImpl) AddLayer(blob cpi.BlobAccess, d *cpi.Descriptor) (int, error) { + if a.IsReadOnly() { + return -1, accessio.ErrReadOnly + } + m, err := a.Manifest() + if err != nil { + return -1, err + } + + a.lock.Lock() + defer a.lock.Unlock() + if d == nil { + d = &artdesc.Descriptor{} + } + d.Digest = blob.Digest() + d.Size = blob.Size() + if d.MediaType == "" { + d.MediaType = blob.MimeType() + if d.MediaType == "" { + d.MediaType = artdesc.MediaTypeImageLayer + r, err := blob.Reader() + if err != nil { + return -1, err + } + defer r.Close() + zr, err := gzip.NewReader(r) + if err == nil { + err = zr.Close() + if err == nil { + d.MediaType = artdesc.MediaTypeImageLayerGzip + } + } + } + } + + err = a.container.AddBlob(blob) + if err != nil { + return -1, err + } + + m.Layers = append(m.Layers, *d) + return len(m.Layers) - 1, nil +} + +func AdjustSize(d *artdesc.Descriptor, size int64) error { + if size != blobaccess.BLOB_UNKNOWN_SIZE { + if d.Size == blobaccess.BLOB_UNKNOWN_SIZE { + d.Size = size + } else if d.Size != size { + return errors.Newf("blob size mismatch %d != %d", size, d.Size) + } + } + return nil +} diff --git a/pkg/contexts/oci/cpi/support/artifactsetblobaccess.go b/api/oci/cpi/support/artifactsetblobaccess.go similarity index 91% rename from pkg/contexts/oci/cpi/support/artifactsetblobaccess.go rename to api/oci/cpi/support/artifactsetblobaccess.go index 425c2d8cc..cf252258d 100644 --- a/pkg/contexts/oci/cpi/support/artifactsetblobaccess.go +++ b/api/oci/cpi/support/artifactsetblobaccess.go @@ -5,9 +5,9 @@ import ( "github.com/opencontainers/go-digest" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) //////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/contexts/oci/cpi/support/base.go b/api/oci/cpi/support/base.go similarity index 86% rename from pkg/contexts/oci/cpi/support/base.go rename to api/oci/cpi/support/base.go index c4a32c630..60cbf0a79 100644 --- a/pkg/contexts/oci/cpi/support/base.go +++ b/api/oci/cpi/support/base.go @@ -7,10 +7,10 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/opencontainers/go-digest" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type artifactBase struct { diff --git a/pkg/contexts/oci/cpi/support/flavor_index.go b/api/oci/cpi/support/flavor_index.go similarity index 88% rename from pkg/contexts/oci/cpi/support/flavor_index.go rename to api/oci/cpi/support/flavor_index.go index 450fce7ca..971a487dc 100644 --- a/pkg/contexts/oci/cpi/support/flavor_index.go +++ b/api/oci/cpi/support/flavor_index.go @@ -4,11 +4,11 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/opencontainers/go-digest" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" - "github.com/open-component-model/ocm/pkg/contexts/oci/internal" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/internal" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" ) type IndexAccess struct { diff --git a/pkg/contexts/oci/cpi/support/flavor_manifest.go b/api/oci/cpi/support/flavor_manifest.go similarity index 94% rename from pkg/contexts/oci/cpi/support/flavor_manifest.go rename to api/oci/cpi/support/flavor_manifest.go index 02603de87..a6e596afb 100644 --- a/pkg/contexts/oci/cpi/support/flavor_manifest.go +++ b/api/oci/cpi/support/flavor_manifest.go @@ -6,9 +6,9 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/opencontainers/go-digest" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc" - "github.com/open-component-model/ocm/pkg/contexts/oci/cpi" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/utils/accessobj" ) type ManifestAccess struct { diff --git a/api/oci/cpi/support/namespace.go b/api/oci/cpi/support/namespace.go new file mode 100644 index 000000000..3c655605d --- /dev/null +++ b/api/oci/cpi/support/namespace.go @@ -0,0 +1,113 @@ +package support + +import ( + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/refmgmt" +) + +// BlobProvider manages the technical access to blobs. +type BlobProvider interface { + refmgmt.Allocatable + cpi.BlobSource + cpi.BlobSink +} + +// NamespaceContainer is the interface used by subsequent access objects +// to access the base implementation. +type NamespaceContainer interface { + SetImplementation(impl NamespaceAccessImpl) + + IsReadOnly() bool + // IsClosed() bool + + cpi.BlobSource + cpi.BlobSink + + Close() error + + // GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor + + GetArtifact(i NamespaceAccessImpl, vers string) (cpi.ArtifactAccess, error) + NewArtifact(i NamespaceAccessImpl, arts ...cpi.Artifact) (cpi.ArtifactAccess, error) + + AddArtifact(artifact cpi.Artifact, tags ...string) (access blobaccess.BlobAccess, err error) + + AddTags(digest digest.Digest, tags ...string) error + ListTags() ([]string, error) + HasArtifact(vers string) (bool, error) +} + +//////////////////////////////////////////////////////////////////////////////// + +type NamespaceAccessImpl interface { + cpi.NamespaceAccessImpl + + // GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor + IsReadOnly() bool + + WithContainer(container NamespaceContainer) NamespaceAccessImpl +} + +type namespaceAccessImpl struct { + *cpi.NamespaceAccessImplBase + NamespaceContainer // inherit as many as possible methods for cpi.NamespaceAccessImpl +} + +var _ NamespaceAccessImpl = (*namespaceAccessImpl)(nil) + +func NewNamespaceAccessImpl(namespace string, c NamespaceContainer, repo cpi.RepositoryViewManager) (NamespaceAccessImpl, error) { + base, err := cpi.NewNamespaceAccessImplBase(namespace, repo) + if err != nil { + return nil, err + } + impl := &namespaceAccessImpl{ + NamespaceAccessImplBase: base, + NamespaceContainer: c, + } + + c.SetImplementation(impl) + return impl, nil +} + +func (n *namespaceAccessImpl) Close() error { + return accessio.Close(n.NamespaceAccessImplBase, n.NamespaceContainer) +} + +func NewNamespaceAccess(namespace string, c NamespaceContainer, repo cpi.RepositoryViewManager, kind ...string) (cpi.NamespaceAccess, error) { + impl, err := NewNamespaceAccessImpl(namespace, c, repo) + if err != nil { + return nil, err + } + return cpi.NewNamespaceAccess(impl, kind...), nil +} + +func GetArtifactSetContainer(i cpi.NamespaceAccessImpl) (NamespaceContainer, error) { + if c, ok := i.(*namespaceAccessImpl); ok { + return c.NamespaceContainer, nil + } + return nil, errors.ErrNotSupported() +} + +func (i *namespaceAccessImpl) WithContainer(c NamespaceContainer) NamespaceAccessImpl { + return &namespaceAccessImpl{ + NamespaceAccessImplBase: i.NamespaceAccessImplBase, + NamespaceContainer: c, + } +} + +func (i *namespaceAccessImpl) GetArtifact(vers string) (cpi.ArtifactAccess, error) { + return i.NamespaceContainer.GetArtifact(i, vers) +} + +func (i *namespaceAccessImpl) AddArtifact(artifact cpi.Artifact, tags ...string) (access blobaccess.BlobAccess, err error) { + return i.NamespaceContainer.AddArtifact(artifact, tags...) +} + +func (i *namespaceAccessImpl) NewArtifact(arts ...cpi.Artifact) (cpi.ArtifactAccess, error) { + return i.NamespaceContainer.NewArtifact(i, arts...) +} diff --git a/api/oci/cpi/utils.go b/api/oci/cpi/utils.go new file mode 100644 index 000000000..5b6623169 --- /dev/null +++ b/api/oci/cpi/utils.go @@ -0,0 +1,63 @@ +package cpi + +import ( + "strings" + + "ocm.software/ocm/api/oci/grammar" +) + +type StringList []string + +func (s *StringList) Add(n string) { + for _, e := range *s { + if n == e { + return + } + } + *s = append(*s, n) +} + +func FilterByNamespacePrefix(prefix string, list []string) []string { + result := []string{} + sub := prefix + if prefix != "" && !strings.HasSuffix(prefix, grammar.RepositorySeparator) { + sub = prefix + grammar.RepositorySeparator + } + for _, k := range list { + if k == prefix || strings.HasPrefix(k, sub) { + result = append(result, k) + } + } + return result +} + +func FilterChildren(closure bool, prefix string, list []string) []string { + if closure { + return FilterByNamespacePrefix(prefix, list) + } + sub := prefix + if prefix != "" && !strings.HasSuffix(prefix, grammar.RepositorySeparator) { + sub = prefix + grammar.RepositorySeparator + } + set := map[string]bool{} + for _, n := range list { + if n == prefix { + set[n] = true + } else if strings.HasPrefix(n, sub) { + rest := n[len(sub):] + i := strings.Index(rest, grammar.RepositorySeparator) + if i < 0 { + set[n] = true + } else { + set[n[:i+len(sub)]] = true + } + } + } + result := make([]string, 0, len(set)) + for _, n := range list { + if set[n] { + result = append(result, n) + } + } + return result +} diff --git a/api/oci/cpi/utils_test.go b/api/oci/cpi/utils_test.go new file mode 100644 index 000000000..0791a6f4e --- /dev/null +++ b/api/oci/cpi/utils_test.go @@ -0,0 +1,62 @@ +package cpi_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/oci/cpi" +) + +var _ = Describe("OCI CPI utils", func() { + list := []string{ + "a/b/c/d", + "a/b/c", + "a/b", + "a/c/d", + "a/c", + "b/c", + } + + It("calculates exclusive number", func() { + Expect(cpi.FilterByNamespacePrefix("a/b/", list)).To(Equal([]string{ + "a/b/c/d", + "a/b/c", + })) + }) + It("calculates inclusive number", func() { + Expect(cpi.FilterByNamespacePrefix("a/b", list)).To(Equal([]string{ + "a/b/c/d", + "a/b/c", + "a/b", + })) + }) + + It("calculates closure", func() { + Expect(cpi.FilterChildren(true, "a/b", list)).To(Equal([]string{ + "a/b/c/d", + "a/b/c", + "a/b", + })) + }) + + It("calculates children", func() { + Expect(cpi.FilterChildren(false, "a/b/", list)).To(Equal([]string{ + "a/b/c", + })) + }) + + It("calculates inclusive children", func() { + Expect(cpi.FilterChildren(false, "a/b", list)).To(Equal([]string{ + "a/b/c", + "a/b", + })) + }) + + It("calculates children closure", func() { + Expect(cpi.FilterChildren(true, "a/b", cpi.FilterByNamespacePrefix("a/b", list))).To(Equal([]string{ + "a/b/c/d", + "a/b/c", + "a/b", + })) + }) +}) diff --git a/api/oci/cpi/view.go b/api/oci/cpi/view.go new file mode 100644 index 000000000..c32e7a14c --- /dev/null +++ b/api/oci/cpi/view.go @@ -0,0 +1,397 @@ +package cpi + +import ( + "fmt" + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/internal" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/refmgmt/resource" +) + +var ErrClosed = resource.ErrClosed + +//////////////////////////////////////////////////////////////////////////////// + +type _RepositoryView interface { + resource.ResourceViewInt[Repository] // here you have to redeclare +} + +type RepositoryViewManager = resource.ViewManager[Repository] // here you have to use an alias + +type RepositoryImpl interface { + internal.RepositoryImpl + resource.ResourceImplementation[Repository] +} + +type _RepositoryImplBase = resource.ResourceImplBase[Repository] + +type RepositoryImplBase struct { + _RepositoryImplBase + ctx Context +} + +func (b *RepositoryImplBase) GetContext() Context { + return b.ctx +} + +func NewRepositoryImplBase(ctx Context) RepositoryImplBase { + return RepositoryImplBase{ + _RepositoryImplBase: resource.ResourceImplBase[Repository]{}, + ctx: ctx, + } +} + +type repositoryView struct { + _RepositoryView + impl RepositoryImpl +} + +var ( + _ Repository = (*repositoryView)(nil) + _ internal.ConsumerIdentityProvider = (*repositoryView)(nil) +) + +func GetRepositoryImplementation(n Repository) (RepositoryImpl, error) { + if v, ok := n.(*repositoryView); ok { + return v.impl, nil + } + return nil, errors.ErrNotSupported("repository implementation type", fmt.Sprintf("%T", n)) +} + +func repositoryViewCreator(i RepositoryImpl, v resource.CloserView, d RepositoryViewManager) Repository { + return &repositoryView{ + _RepositoryView: resource.NewView[Repository](v, d), + impl: i, + } +} + +func NewRepository(impl RepositoryImpl, name ...string) Repository { + return resource.NewResource[Repository](impl, repositoryViewCreator, utils.OptionalDefaulted("OCI repo", name...), true) +} + +func (r *repositoryView) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity { + return credentials.GetProvidedConsumerId(r.impl, uctx...) +} + +func (r *repositoryView) GetIdentityMatcher() string { + return credentials.GetProvidedIdentityMatcher(r.impl) +} + +func (r *repositoryView) GetSpecification() internal.RepositorySpec { + return r.impl.GetSpecification() +} + +func (r *repositoryView) GetContext() Context { + return r.impl.GetContext() +} + +func (r *repositoryView) NamespaceLister() (lister internal.NamespaceLister) { + return r.impl.NamespaceLister() +} + +func (r *repositoryView) ExistsArtifact(name string, ref string) (ok bool, err error) { + err = r.Execute(func() error { + ok, err = r.impl.ExistsArtifact(name, ref) + return err + }) + return ok, err +} + +func (r *repositoryView) LookupArtifact(name string, ref string) (acc ArtifactAccess, err error) { + err = r.Execute(func() error { + acc, err = r.impl.LookupArtifact(name, ref) + return err + }) + return acc, err +} + +func (r *repositoryView) LookupNamespace(name string) (acc NamespaceAccess, err error) { + err = r.Execute(func() error { + acc, err = r.impl.LookupNamespace(name) + return err + }) + return acc, err +} + +//////////////////////////////////////////////////////////////////////////////// + +type _NamespaceAccessView interface { + resource.ResourceViewInt[NamespaceAccess] // here you have to redeclare +} + +type NamespaceAccessViewManager = resource.ViewManager[NamespaceAccess] // here you have to use an alias + +type NamespaceAccessImpl interface { + internal.NamespaceAccessImpl + + resource.ResourceImplementation[NamespaceAccess] + + GetNamespace() string +} + +type _NamespaceAccessImplBase = resource.ResourceImplBase[NamespaceAccess] + +type NamespaceAccessImplBase struct { + *_NamespaceAccessImplBase + namespace string +} + +func NewNamespaceAccessImplBase(namespace string, repo RepositoryViewManager, closer ...io.Closer) (*NamespaceAccessImplBase, error) { + base, err := resource.NewResourceImplBase[NamespaceAccess](repo, closer...) + if err != nil { + return nil, err + } + return &NamespaceAccessImplBase{ + _NamespaceAccessImplBase: base, + namespace: namespace, + }, nil +} + +func (b *NamespaceAccessImplBase) GetNamespace() string { + return b.namespace +} + +type namespaceAccessView struct { + _NamespaceAccessView + impl NamespaceAccessImpl +} + +var _ NamespaceAccess = (*namespaceAccessView)(nil) + +func GetNamespaceAccessImplementation(n NamespaceAccess) (NamespaceAccessImpl, error) { + if v, ok := n.(*namespaceAccessView); ok { + return v.impl, nil + } + return nil, errors.ErrNotSupported("namespace implementation type", fmt.Sprintf("%T", n)) +} + +func namespaceAccessViewCreator(i NamespaceAccessImpl, v resource.CloserView, d NamespaceAccessViewManager) NamespaceAccess { + return &namespaceAccessView{ + _NamespaceAccessView: resource.NewView[NamespaceAccess](v, d), + impl: i, + } +} + +func NewNamespaceAccess(impl NamespaceAccessImpl, kind ...string) NamespaceAccess { + return resource.NewResource[NamespaceAccess](impl, namespaceAccessViewCreator, fmt.Sprintf("%s %s", utils.OptionalDefaulted("namespace", kind...), impl.GetNamespace()), true) +} + +func (n *namespaceAccessView) GetNamespace() string { + return n.impl.GetNamespace() +} + +func (n *namespaceAccessView) GetArtifact(version string) (acc internal.ArtifactAccess, err error) { + err = n.Execute(func() error { + acc, err = n.impl.GetArtifact(version) + return err + }) + return acc, err +} + +func (n *namespaceAccessView) GetBlobData(digest digest.Digest) (size int64, acc internal.DataAccess, err error) { + err = n.Execute(func() error { + size, acc, err = n.impl.GetBlobData(digest) + return err + }) + return size, acc, err +} + +func (n *namespaceAccessView) AddBlob(access internal.BlobAccess) error { + return n.Execute(func() error { + return n.impl.AddBlob(access) + }) +} + +func (n *namespaceAccessView) HasArtifact(vers string) (ok bool, err error) { + err = n.Execute(func() error { + ok, err = n.impl.HasArtifact(vers) + return err + }) + return ok, err +} + +func (n *namespaceAccessView) AddArtifact(a internal.Artifact, tags ...string) (acc internal.BlobAccess, err error) { + err = n.Execute(func() error { + acc, err = n.impl.AddArtifact(a, tags...) + return err + }) + return acc, err +} + +func (n *namespaceAccessView) AddTags(digest digest.Digest, tags ...string) error { + return n.Execute(func() error { + return n.impl.AddTags(digest, tags...) + }) +} + +func (n *namespaceAccessView) ListTags() (list []string, err error) { + err = n.Execute(func() error { + list, err = n.impl.ListTags() + return err + }) + return list, err +} + +func (n *namespaceAccessView) NewArtifact(artifact ...Artifact) (acc internal.ArtifactAccess, err error) { + err = n.Execute(func() error { + acc, err = n.impl.NewArtifact(artifact...) + return err + }) + return acc, err +} + +//////////////////////////////////////////////////////////////////////////////// + +type _ArtifactAccessView interface { + resource.ResourceViewInt[ArtifactAccess] +} + +type ArtifactAccessViewManager = resource.ViewManager[ArtifactAccess] + +type ArtifactAccessImpl interface { + internal.ArtifactAccessImpl + + resource.ResourceImplementation[ArtifactAccess] + + // creation of slave objects require the original view they are created for. + + ManifestAccess(ArtifactAccess) ManifestAccess + IndexAccess(ArtifactAccess) IndexAccess +} + +type ArtifactAccessImplBase = resource.ResourceImplBase[ArtifactAccess] + +func NewArtifactAccessImplBase(ns NamespaceAccessViewManager, closer ...io.Closer) (*ArtifactAccessImplBase, error) { + return resource.NewResourceImplBase[ArtifactAccess](ns, closer...) +} + +type artifactAccessView struct { + _ArtifactAccessView + impl ArtifactAccessImpl +} + +var _ ArtifactAccess = (*artifactAccessView)(nil) + +func artifactAccessViewCreator(i ArtifactAccessImpl, v resource.CloserView, d resource.ViewManager[ArtifactAccess]) ArtifactAccess { + return &artifactAccessView{ + _ArtifactAccessView: resource.NewView[ArtifactAccess](v, d), + impl: i, + } +} + +func NewArtifactAccess(impl ArtifactAccessImpl) ArtifactAccess { + return resource.NewResource[ArtifactAccess](impl, artifactAccessViewCreator, "artifact", true) +} + +func (a *artifactAccessView) IsManifest() bool { + return a.impl.IsManifest() +} + +func (a *artifactAccessView) IsIndex() bool { + return a.impl.IsIndex() +} + +func (a *artifactAccessView) IsValid() bool { + return a.impl.IsValid() +} + +func (a *artifactAccessView) Digest() digest.Digest { + return a.impl.Digest() +} + +func (a *artifactAccessView) Blob() (internal.BlobAccess, error) { + return a.impl.Blob() +} + +func (a *artifactAccessView) GetDescriptor() *artdesc.Artifact { + return a.impl.GetDescriptor() +} + +func (a *artifactAccessView) Artifact() *artdesc.Artifact { + return a.impl.Artifact() +} + +func (a *artifactAccessView) Manifest() (*artdesc.Manifest, error) { + return a.impl.Manifest() +} + +func (a *artifactAccessView) ManifestAccess() internal.ManifestAccess { + return a.impl.ManifestAccess(a) +} + +func (a *artifactAccessView) Index() (*artdesc.Index, error) { + return a.impl.Index() +} + +func (a *artifactAccessView) IndexAccess() internal.IndexAccess { + return a.impl.IndexAccess(a) +} + +func (a *artifactAccessView) GetBlobData(digest digest.Digest) (size int64, acc internal.DataAccess, err error) { + size = -1 + err = a.Execute(func() error { + size, acc, err = a.impl.GetBlobData(digest) + return err + }) + return size, acc, err +} + +func (a *artifactAccessView) AddBlob(access internal.BlobAccess) error { + if err := utils.ValidateObject(access); err != nil { + return err + } + return a.Execute(func() error { + return a.impl.AddBlob(access) + }) +} + +func (a *artifactAccessView) GetBlob(digest digest.Digest) (acc internal.BlobAccess, err error) { + err = a.Execute(func() error { + acc, err = a.impl.GetBlob(digest) + return err + }) + return acc, err +} + +func (a *artifactAccessView) GetArtifact(digest digest.Digest) (acc internal.ArtifactAccess, err error) { + err = a.Execute(func() error { + acc, err = a.impl.GetArtifact(digest) + return err + }) + return acc, err +} + +func (a *artifactAccessView) AddArtifact(artifact internal.Artifact, platform *artdesc.Platform) (acc internal.BlobAccess, err error) { + err = a.Execute(func() error { + acc, err = a.impl.AddArtifact(artifact, platform) + return err + }) + return acc, err +} + +func (a *artifactAccessView) NewArtifact(art ...Artifact) (acc ArtifactAccess, err error) { + err = a.Execute(func() error { + acc, err = a.impl.NewArtifact(art...) + return err + }) + return acc, err +} + +func (a *artifactAccessView) AddLayer(access internal.BlobAccess, descriptor *artdesc.Descriptor) (index int, err error) { + if err := utils.ValidateObject(access); err != nil { + return -1, err + } + + index = -1 + err = a.Execute(func() error { + index, err = a.impl.AddLayer(access, descriptor) + return err + }) + return index, err +} diff --git a/api/oci/extensions/actions/init.go b/api/oci/extensions/actions/init.go new file mode 100644 index 000000000..3d0109d5d --- /dev/null +++ b/api/oci/extensions/actions/init.go @@ -0,0 +1,5 @@ +package actions + +import ( + _ "ocm.software/ocm/api/oci/extensions/actions/oci-repository-prepare" +) diff --git a/api/oci/extensions/actions/oci-repository-prepare/exec.go b/api/oci/extensions/actions/oci-repository-prepare/exec.go new file mode 100644 index 000000000..cb2b75367 --- /dev/null +++ b/api/oci/extensions/actions/oci-repository-prepare/exec.go @@ -0,0 +1,12 @@ +package oci_repository_prepare + +import ( + "github.com/mandelsoft/goutils/generics" + + "ocm.software/ocm/api/datacontext/action/handlers" + common "ocm.software/ocm/api/utils/misc" +) + +func Execute(hdlrs handlers.Registry, host, repo string, creds common.Properties) (*ActionResult, error) { + return generics.CastR[*ActionResult](hdlrs.Execute(Spec(host, repo), creds)) +} diff --git a/api/oci/extensions/actions/oci-repository-prepare/type.go b/api/oci/extensions/actions/oci-repository-prepare/type.go new file mode 100644 index 000000000..d3b160f5b --- /dev/null +++ b/api/oci/extensions/actions/oci-repository-prepare/type.go @@ -0,0 +1,81 @@ +package oci_repository_prepare + +import ( + "path" + + "ocm.software/ocm/api/credentials/builtin/oci/identity" + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/datacontext/action/api" + "ocm.software/ocm/api/utils" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/runtime" +) + +const Type = "oci.repository.prepare" + +func init() { + api.RegisterAction(Type, "Prepare the usage of a repository in an OCI registry.", usage, + []string{identity.ID_HOSTNAME, identity.ID_PORT, identity.ID_PATHPREFIX}) + + api.RegisterType(api.NewActionType[*ActionSpecV1, *ActionResultV1](Type, "v1")) +} + +var usage = ` +The hostname of the target repository is used as selector. The action should +assure, that the requested repository is available on the target OCI registry. + +Spec version v1 uses the following specification fields: +-
hostname
*string*: The hostname of the OCI registry.
+- repository
*string*: The OCI repository name.
+`
+
+////////////////////////////////////////////////////////////////////////////////
+// internal version
+
+type ActionSpec = ActionSpecV1
+
+type ActionResult = ActionResultV1
+
+func Spec(host string, repo string) *ActionSpec {
+ return &ActionSpec{
+ ObjectVersionedType: runtime.ObjectVersionedType{runtime.TypeName(Type, "v1")},
+ Hostname: host,
+ Repository: repo,
+ }
+}
+
+func Result(msg string) *ActionResult {
+ return &ActionResult{
+ CommonResult: api.CommonResult{
+ ObjectVersionedType: runtime.ObjectVersionedType{runtime.TypeName(Type, "v1")},
+ Message: msg,
+ },
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// serialization formats
+
+type ActionSpecV1 struct {
+ runtime.ObjectVersionedType
+ Hostname string `json:"hostname"`
+ Repository string `json:"repository"`
+}
+
+func (s *ActionSpecV1) Selector() api.Selector {
+ return api.Selector(s.Hostname)
+}
+
+func (s *ActionSpecV1) GetConsumerAttributes() common.Properties {
+ host, port, base := utils.SplitLocator(s.Hostname)
+ return common.Properties{
+ cpi.ID_TYPE: identity.CONSUMER_TYPE,
+ identity.ID_HOSTNAME: host,
+ identity.ID_PATHPREFIX: path.Join(base, s.Repository),
+ identity.ID_PORT: port,
+ }
+}
+
+type ActionResultV1 struct {
+ api.CommonResult `json:",inline"`
+}
diff --git a/api/oci/extensions/attrs/cacheattr/attr.go b/api/oci/extensions/attrs/cacheattr/attr.go
new file mode 100644
index 000000000..054423207
--- /dev/null
+++ b/api/oci/extensions/attrs/cacheattr/attr.go
@@ -0,0 +1,75 @@
+package cacheattr
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "github.com/mandelsoft/oci/cache"
+ ATTR_SHORT = "cache"
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*string*
+Filesystem folder to use for caching OCI blobs
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ if _, ok := v.(accessio.BlobCache); !ok {
+ return nil, fmt.Errorf("accessio.BlobCache required")
+ }
+ return nil, nil
+}
+
+func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) {
+ var value string
+ err := unmarshaller.Unmarshal(data, &value)
+ if value != "" {
+ value, err = utils.ResolvePath(value)
+ if err != nil {
+ return nil, err
+ }
+ // TODO: This should use the virtual filesystem.
+ err = os.MkdirAll(value, 0o700)
+ if err == nil {
+ return accessio.NewStaticBlobCache(value)
+ }
+ } else if err == nil {
+ err = errors.Newf("file path missing")
+ }
+ return value, err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx datacontext.Context) accessio.BlobCache {
+ a := ctx.GetAttributes().GetAttribute(ATTR_KEY)
+ if a == nil {
+ return nil
+ }
+ return a.(accessio.BlobCache)
+}
+
+func Set(ctx datacontext.Context, cache accessio.BlobCache) error {
+ return ctx.GetAttributes().SetAttribute(ATTR_KEY, cache)
+}
diff --git a/api/oci/extensions/attrs/cacheattr/attr_test.go b/api/oci/extensions/attrs/cacheattr/attr_test.go
new file mode 100644
index 000000000..2a42e035f
--- /dev/null
+++ b/api/oci/extensions/attrs/cacheattr/attr_test.go
@@ -0,0 +1,56 @@
+package cacheattr_test
+
+import (
+ "os"
+ "reflect"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/extensions/attrs/cacheattr"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+var _ = Describe("attribute", func() {
+ var ctx ocm.Context
+ var cfgctx config.Context
+ var cache accessio.BlobCache
+
+ BeforeEach(func() {
+ var err error
+ cfgctx = config.WithSharedAttributes(datacontext.New(nil)).New()
+ credctx := credentials.WithConfigs(cfgctx).New()
+ ocictx := oci.WithCredentials(credctx).New()
+ ctx = ocm.WithOCIRepositories(ocictx).New()
+ cache, err = accessio.NewDefaultBlobCache()
+ Expect(err).To(Succeed())
+ })
+ AfterEach(func() {
+ cache.Unref()
+ })
+
+ It("local setting", func() {
+ Expect(cacheattr.Get(ctx)).To(BeNil())
+ Expect(cacheattr.Set(ctx, cache)).To(Succeed())
+ Expect(cacheattr.Get(ctx)).To(BeIdenticalTo(cache))
+ })
+
+ It("global setting", func() {
+ Expect(cacheattr.Get(cfgctx)).To(BeNil())
+ Expect(cacheattr.Set(ctx, cache)).To(Succeed())
+ Expect(cacheattr.Get(ctx)).To(BeIdenticalTo(cache))
+ })
+
+ It("parses string", func() {
+ dir := os.TempDir()
+ cache, err := cacheattr.AttributeType{}.Decode([]byte(dir), runtime.DefaultYAMLEncoding)
+ Expect(err).To(Succeed())
+ Expect(reflect.TypeOf(cache).String()).To(Equal("*accessio.blobCache"))
+ })
+})
diff --git a/pkg/contexts/oci/attrs/cacheattr/suite_test.go b/api/oci/extensions/attrs/cacheattr/suite_test.go
similarity index 100%
rename from pkg/contexts/oci/attrs/cacheattr/suite_test.go
rename to api/oci/extensions/attrs/cacheattr/suite_test.go
diff --git a/api/oci/extensions/attrs/init.go b/api/oci/extensions/attrs/init.go
new file mode 100644
index 000000000..abc6564a8
--- /dev/null
+++ b/api/oci/extensions/attrs/init.go
@@ -0,0 +1,5 @@
+package attrs
+
+import (
+ _ "ocm.software/ocm/api/oci/extensions/attrs/cacheattr"
+)
diff --git a/pkg/contexts/oci/repositories/artifactset/artifactset.go b/api/oci/extensions/repositories/artifactset/artifactset.go
similarity index 96%
rename from pkg/contexts/oci/repositories/artifactset/artifactset.go
rename to api/oci/extensions/repositories/artifactset/artifactset.go
index b708bc2a4..9a1e337c5 100644
--- a/pkg/contexts/oci/repositories/artifactset/artifactset.go
+++ b/api/oci/extensions/repositories/artifactset/artifactset.go
@@ -7,13 +7,13 @@ import (
"github.com/mandelsoft/vfs/pkg/vfs"
"github.com/opencontainers/go-digest"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/oci/annotations"
- "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc"
- "github.com/open-component-model/ocm/pkg/contexts/oci/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/oci/cpi/support"
+ "ocm.software/ocm/api/oci/annotations"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/cpi/support"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
)
const (
diff --git a/pkg/contexts/oci/repositories/artifactset/artifactset_test.go b/api/oci/extensions/repositories/artifactset/artifactset_test.go
similarity index 94%
rename from pkg/contexts/oci/repositories/artifactset/artifactset_test.go
rename to api/oci/extensions/repositories/artifactset/artifactset_test.go
index d49b03050..9b5562383 100644
--- a/pkg/contexts/oci/repositories/artifactset/artifactset_test.go
+++ b/api/oci/extensions/repositories/artifactset/artifactset_test.go
@@ -8,17 +8,17 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset/testhelper"
- . "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf/testhelper"
+ . "ocm.software/ocm/api/oci/extensions/repositories/artifactset/testhelper"
+ . "ocm.software/ocm/api/oci/extensions/repositories/ctf/testhelper"
"github.com/mandelsoft/goutils/finalizer"
"github.com/mandelsoft/vfs/pkg/osfs"
"github.com/mandelsoft/vfs/pkg/vfs"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset"
- "github.com/open-component-model/ocm/pkg/mime"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
)
func defaultManifestFill(a *artifactset.ArtifactSet) {
diff --git a/pkg/contexts/oci/repositories/artifactset/error.go b/api/oci/extensions/repositories/artifactset/error.go
similarity index 100%
rename from pkg/contexts/oci/repositories/artifactset/error.go
rename to api/oci/extensions/repositories/artifactset/error.go
diff --git a/api/oci/extensions/repositories/artifactset/filesystemaccess.go b/api/oci/extensions/repositories/artifactset/filesystemaccess.go
new file mode 100644
index 000000000..99387d733
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/filesystemaccess.go
@@ -0,0 +1,45 @@
+package artifactset
+
+import (
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/cpi/support"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+type FileSystemBlobAccess struct {
+ *accessobj.FileSystemBlobAccess
+}
+
+func NewFileSystemBlobAccess(access *accessobj.AccessObject) *FileSystemBlobAccess {
+ return &FileSystemBlobAccess{accessobj.NewFileSystemBlobAccess(access)}
+}
+
+func (i *FileSystemBlobAccess) GetArtifact(access support.NamespaceAccessImpl, digest digest.Digest) (acc cpi.ArtifactAccess, err error) {
+ v, err := access.View()
+ if err != nil {
+ return nil, err
+ }
+ defer v.Close()
+ _, data, err := i.GetBlobData(digest)
+ if err == nil {
+ blob := blobaccess.ForDataAccess("", -1, "", data)
+ acc, err = support.NewArtifactForBlob(access, blob)
+ }
+ return
+}
+
+func (i *FileSystemBlobAccess) AddArtifactBlob(artifact cpi.Artifact) (cpi.BlobAccess, error) {
+ blob, err := artifact.Blob()
+ if err != nil {
+ return nil, err
+ }
+
+ err = i.AddBlob(blob)
+ if err != nil {
+ return nil, err
+ }
+ return blob, nil
+}
diff --git a/api/oci/extensions/repositories/artifactset/format.go b/api/oci/extensions/repositories/artifactset/format.go
new file mode 100644
index 000000000..485f97dbd
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/format.go
@@ -0,0 +1,269 @@
+package artifactset
+
+import (
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ mime2 "ocm.software/ocm/api/utils/mime"
+)
+
+const (
+ // The artifact descriptor name for artifact format.
+ ArtifactSetDescriptorFileName = "artifact-descriptor.json"
+ BlobsDirectoryName = "blobs"
+
+ OCIArtifactSetDescriptorFileName = "index.json"
+ OCILayouFileName = "oci-layout"
+)
+
+var DefaultArtifactSetDescriptorFileName = OCIArtifactSetDescriptorFileName
+
+func IsOCIDefaultFormat() bool {
+ return DefaultArtifactSetDescriptorFileName == OCIArtifactSetDescriptorFileName
+}
+
+func DescriptorFileName(format string) string {
+ switch format {
+ case FORMAT_OCI:
+ return OCIArtifactSetDescriptorFileName
+ case FORMAT_OCM:
+ return ArtifactSetDescriptorFileName
+ case "":
+ return DefaultArtifactSetDescriptorFileName
+ }
+ return ""
+}
+
+type accessObjectInfo struct {
+ accessobj.DefaultAccessObjectInfo
+}
+
+var _ accessobj.AccessObjectInfo = (*accessObjectInfo)(nil)
+
+func NewAccessObjectInfo(fmts ...string) accessobj.AccessObjectInfo {
+ a := &accessObjectInfo{
+ accessobj.DefaultAccessObjectInfo{
+ ObjectTypeName: "artifactset",
+ ElementDirectoryName: BlobsDirectoryName,
+ ElementTypeName: "blob",
+ DescriptorHandlerFactory: NewStateHandler,
+ },
+ }
+ oci := IsOCIDefaultFormat()
+ if len(fmts) > 0 {
+ switch fmts[0] {
+ case FORMAT_OCM:
+ oci = false
+ case FORMAT_OCI:
+ oci = true
+ case "":
+ }
+ }
+ if oci {
+ a.setOCI()
+ } else {
+ a.setOCM()
+ }
+ return a
+}
+
+func (a *accessObjectInfo) setOCI() {
+ a.DescriptorFileName = OCIArtifactSetDescriptorFileName
+ a.AdditionalFiles = []string{OCILayouFileName}
+}
+
+func (a *accessObjectInfo) setOCM() {
+ a.DescriptorFileName = ArtifactSetDescriptorFileName
+ a.AdditionalFiles = nil
+}
+
+func (a *accessObjectInfo) setupOCIFS(fs vfs.FileSystem, mode vfs.FileMode) error {
+ data := `{
+ "imageLayoutVersion": "1.0.0"
+}
+`
+ return vfs.WriteFile(fs, OCILayouFileName, []byte(data), mode)
+}
+
+func (a *accessObjectInfo) SetupFileSystem(fs vfs.FileSystem, mode vfs.FileMode) error {
+ if err := a.SetupFor(fs); err != nil {
+ return err
+ }
+ if err := a.DefaultAccessObjectInfo.SetupFileSystem(fs, mode); err != nil {
+ return err
+ }
+ if len(a.AdditionalFiles) > 0 {
+ return a.setupOCIFS(fs, mode)
+ }
+ return nil
+}
+
+func (a *accessObjectInfo) SetupFor(fs vfs.FileSystem) error {
+ ok, err := vfs.FileExists(fs, OCIArtifactSetDescriptorFileName)
+ if err != nil {
+ return err
+ }
+ if ok {
+ a.setOCI()
+ return nil
+ }
+
+ ok, err = vfs.FileExists(fs, ArtifactSetDescriptorFileName)
+ if err != nil {
+ return err
+ }
+ if ok {
+ a.setOCM()
+ return nil
+ }
+
+ ok, err = vfs.FileExists(fs, OCILayouFileName)
+ if err != nil {
+ return err
+ }
+ if ok {
+ a.setOCI()
+ return nil
+ }
+
+ // keep configured format
+ return nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type Object = ArtifactSet
+
+type FormatHandler interface {
+ accessio.Option
+
+ Format() accessio.FileFormat
+
+ Open(acc accessobj.AccessMode, path string, opts accessio.Options) (*Object, error)
+ Create(path string, opts accessio.Options, mode vfs.FileMode) (*Object, error)
+ Write(obj *Object, path string, opts accessio.Options, mode vfs.FileMode) error
+}
+
+type formatHandler struct {
+ accessobj.FormatHandler
+}
+
+var (
+ FormatDirectory = RegisterFormat(accessobj.FormatDirectory)
+ FormatTAR = RegisterFormat(accessobj.FormatTAR)
+ FormatTGZ = RegisterFormat(accessobj.FormatTGZ)
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+var (
+ fileFormats = map[accessio.FileFormat]FormatHandler{}
+ lock sync.RWMutex
+)
+
+func RegisterFormat(f accessobj.FormatHandler) FormatHandler {
+ lock.Lock()
+ defer lock.Unlock()
+ h := &formatHandler{f}
+ fileFormats[f.Format()] = h
+ return h
+}
+
+func GetFormats() []string {
+ lock.RLock()
+ defer lock.RUnlock()
+ return accessio.GetFormatsFor(fileFormats)
+}
+
+func GetFormat(name accessio.FileFormat) FormatHandler {
+ lock.RLock()
+ defer lock.RUnlock()
+ return fileFormats[name]
+}
+
+func SupportedFormats() []accessio.FileFormat {
+ lock.RLock()
+ defer lock.RUnlock()
+ result := make([]accessio.FileFormat, 0, len(fileFormats))
+ for f := range fileFormats {
+ result = append(result, f)
+ }
+ return result
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func OpenFromBlob(acc accessobj.AccessMode, blob blobaccess.BlobAccess, opts ...accessio.Option) (*Object, error) {
+ return OpenFromDataAccess(acc, blob.MimeType(), blob, opts...)
+}
+
+func OpenFromDataAccess(acc accessobj.AccessMode, mime string, data blobaccess.DataAccess, opts ...accessio.Option) (*Object, error) {
+ o, err := accessio.AccessOptions(nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if o.GetFile() != nil || o.GetReader() != nil {
+ return nil, errors.ErrInvalid("file or reader option not possible for blob access")
+ }
+ reader, err := data.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer reader.Close()
+ o.SetReader(reader)
+ fmt := accessio.FormatTar
+
+ if mime2.IsGZip(mime) {
+ fmt = accessio.FormatTGZ
+ }
+ o.SetFileFormat(fmt)
+ return Open(acc&accessobj.ACC_READONLY, "", 0, o)
+}
+
+func Open(acc accessobj.AccessMode, path string, mode vfs.FileMode, olist ...accessio.Option) (*Object, error) {
+ o, create, err := accessobj.HandleAccessMode(acc, path, &Options{}, olist...)
+ if err != nil {
+ return nil, err
+ }
+ h, ok := fileFormats[*o.GetFileFormat()]
+ if !ok {
+ return nil, errors.ErrUnknown(accessobj.KIND_FILEFORMAT, o.GetFileFormat().String())
+ }
+ if create {
+ return h.Create(path, o, mode)
+ }
+ return h.Open(acc, path, o)
+}
+
+func Create(acc accessobj.AccessMode, path string, mode vfs.FileMode, opts ...accessio.Option) (*Object, error) {
+ o, err := accessio.AccessOptions(&Options{}, opts...)
+ if err != nil {
+ return nil, err
+ }
+ o.DefaultFormat(accessio.FormatDirectory)
+ h, ok := fileFormats[*o.GetFileFormat()]
+ if !ok {
+ return nil, errors.ErrUnknown(accessobj.KIND_FILEFORMAT, o.GetFileFormat().String())
+ }
+ return h.Create(path, o, mode)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func (h *formatHandler) Open(acc accessobj.AccessMode, path string, opts accessio.Options) (*Object, error) {
+ return _Wrap(h.FormatHandler.Open(NewAccessObjectInfo(GetFormatVersion(opts)), acc, path, opts))
+}
+
+func (h *formatHandler) Create(path string, opts accessio.Options, mode vfs.FileMode) (*Object, error) {
+ return _Wrap(h.FormatHandler.Create(NewAccessObjectInfo(GetFormatVersion(opts)), path, opts, mode))
+}
+
+// WriteToFilesystem writes the current object to a filesystem.
+func (h *formatHandler) Write(obj *Object, path string, opts accessio.Options, mode vfs.FileMode) error {
+ return h.FormatHandler.Write(obj.container.base.Access(), path, opts, mode)
+}
diff --git a/api/oci/extensions/repositories/artifactset/options.go b/api/oci/extensions/repositories/artifactset/options.go
new file mode 100644
index 000000000..2760c0168
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/options.go
@@ -0,0 +1,77 @@
+package artifactset
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/utils/accessio"
+)
+
+type Options struct {
+ accessio.StandardOptions
+
+ FormatVersion string `json:"formatVersion,omitempty"`
+}
+
+func NewOptions(olist ...accessio.Option) (*Options, error) {
+ opts := &Options{}
+ err := accessio.ApplyOptions(opts, olist...)
+ if err != nil {
+ return nil, err
+ }
+ return opts, nil
+}
+
+type FormatVersionOption interface {
+ SetFormatVersion(string)
+ GetFormatVersion() string
+}
+
+func GetFormatVersion(opts accessio.Options) string {
+ if o, ok := opts.(FormatVersionOption); ok {
+ return o.GetFormatVersion()
+ }
+ return ""
+}
+
+var _ FormatVersionOption = (*Options)(nil)
+
+func (o *Options) SetFormatVersion(s string) {
+ o.FormatVersion = s
+}
+
+func (o *Options) GetFormatVersion() string {
+ return o.FormatVersion
+}
+
+func (o *Options) ApplyOption(opts accessio.Options) error {
+ err := o.StandardOptions.ApplyOption(opts)
+ if err != nil {
+ return err
+ }
+ if o.FormatVersion != "" {
+ if s, ok := opts.(FormatVersionOption); ok {
+ s.SetFormatVersion(o.FormatVersion)
+ } else {
+ return errors.ErrNotSupported("format version option")
+ }
+ }
+ return nil
+}
+
+type optFmt struct {
+ format string
+}
+
+var _ accessio.Option = (*optFmt)(nil)
+
+func StructureFormat(fmt string) accessio.Option {
+ return &optFmt{fmt}
+}
+
+func (o *optFmt) ApplyOption(opts accessio.Options) error {
+ if s, ok := opts.(FormatVersionOption); ok {
+ s.SetFormatVersion(o.format)
+ return nil
+ }
+ return errors.ErrNotSupported("format version option")
+}
diff --git a/api/oci/extensions/repositories/artifactset/repo_test.go b/api/oci/extensions/repositories/artifactset/repo_test.go
new file mode 100644
index 000000000..afd9c5bbf
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/repo_test.go
@@ -0,0 +1,68 @@
+package artifactset_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/helper/builder"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+var _ = Describe("", func() {
+ var env *builder.Builder
+
+ BeforeEach(func() {
+ env = builder.NewBuilder()
+ // ocmlog.Context().AddRule(logging.NewConditionRule(logging.DebugLevel, accessio.ALLOC_REALM))
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("maps artifact set to repo", func() {
+ env.ArtifactSet("/tmp/set", accessio.FormatDirectory, func() {
+ env.Manifest("v1", func() {
+ env.Config(func() {
+ env.BlobStringData(mime.MIME_JSON, "{}")
+ })
+ env.Layer(func() {
+ env.BlobStringData(mime.MIME_OCTET, "testdata")
+ })
+ })
+ })
+
+ spec, err := artifactset.NewRepositorySpec(accessobj.ACC_READONLY, "/tmp/set", accessio.PathFileSystem(env))
+ Expect(err).To(Succeed())
+
+ r, err := cpi.DefaultContext().RepositoryForSpec(spec)
+ Expect(err).To(Succeed())
+ defer Close(r)
+ ns, err := r.LookupNamespace("")
+ Expect(err).To(Succeed())
+ defer Close(ns)
+
+ Expect(ns.ListTags()).To(Equal([]string{"v1"}))
+
+ a, err := ns.GetArtifact("v1")
+ Expect(err).To(Succeed())
+ defer Close(a)
+
+ Expect(a.IsManifest()).To(BeTrue())
+ m := a.ManifestAccess()
+
+ cfg, err := m.GetConfigBlob()
+ Expect(err).To(Succeed())
+ Expect(cfg.Get()).To(Equal([]byte("{}")))
+
+ Expect(len(m.GetDescriptor().Layers)).To(Equal(1))
+ blob, err := m.GetBlob(m.GetDescriptor().Layers[0].Digest)
+ Expect(err).To(Succeed())
+ Expect(blob.Get()).To(Equal([]byte("testdata")))
+ })
+})
diff --git a/api/oci/extensions/repositories/artifactset/repository.go b/api/oci/extensions/repositories/artifactset/repository.go
new file mode 100644
index 000000000..23368b40f
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/repository.go
@@ -0,0 +1,111 @@
+package artifactset
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+)
+
+type RepositoryImpl struct {
+ cpi.RepositoryImplBase
+ spec *RepositorySpec
+ arch *ArtifactSet
+}
+
+var _ cpi.RepositoryImpl = (*RepositoryImpl)(nil)
+
+func NewRepository(ctx cpi.Context, s *RepositorySpec) (cpi.Repository, error) {
+ if s.PathFileSystem == nil {
+ s.PathFileSystem = vfsattr.Get(ctx)
+ }
+ r := &RepositoryImpl{
+ RepositoryImplBase: cpi.NewRepositoryImplBase(ctx),
+ spec: s,
+ }
+ _, err := r.open()
+ if err != nil {
+ return nil, err
+ }
+ return cpi.NewRepository(r, "OCI artifactset"), nil
+}
+
+func (r *RepositoryImpl) Get() *ArtifactSet {
+ if r.arch != nil {
+ return r.arch
+ }
+ return nil
+}
+
+func (r *RepositoryImpl) open() (*ArtifactSet, error) {
+ a, err := Open(r.spec.AccessMode, r.spec.FilePath, 0o700, &Options{}, &r.spec.Options, accessio.PathFileSystem(r.spec.PathFileSystem))
+ if err != nil {
+ return nil, err
+ }
+ r.arch = a
+ return a, nil
+}
+
+func (r *RepositoryImpl) GetSpecification() cpi.RepositorySpec {
+ return r.spec
+}
+
+func (r *RepositoryImpl) NamespaceLister() cpi.NamespaceLister {
+ return anonymous
+}
+
+func (r *RepositoryImpl) ExistsArtifact(name string, ref string) (bool, error) {
+ if name != "" {
+ return false, nil
+ }
+ return r.arch.HasArtifact(ref)
+}
+
+func (r *RepositoryImpl) LookupArtifact(name string, ref string) (cpi.ArtifactAccess, error) {
+ if name != "" {
+ return nil, cpi.ErrUnknownArtifact(name, ref)
+ }
+ return r.arch.GetArtifact(ref)
+}
+
+func (r *RepositoryImpl) LookupNamespace(name string) (cpi.NamespaceAccess, error) {
+ if name != "" {
+ return nil, errors.ErrNotSupported("namespace", name)
+ }
+ return r.arch.Dup()
+}
+
+func (r RepositoryImpl) Close() error {
+ if r.arch != nil {
+ return r.arch.Close()
+ }
+ return nil
+}
+
+// NamespaceLister handles the namespaces provided by an artifact set.
+// This is always single anonymous namespace, which by ddefinition
+// is the empty string.
+type NamespaceLister struct{}
+
+var anonymous cpi.NamespaceLister = &NamespaceLister{}
+
+// NumNamespaces returns the number of namespaces with a given prefix
+// for an artifact set. This is either one (the anonymous namespace) if
+// the prefix is empty (all namespaces) or zero if a prefix is given.
+func (n *NamespaceLister) NumNamespaces(prefix string) (int, error) {
+ if prefix == "" {
+ return 1, nil
+ }
+ return 0, nil
+}
+
+// GetNamespaces returns namespaces with a given prefix.
+// This is the anonymous namespace ("") for an empty prefix
+// or no namespace at all if a prefix is given.
+func (n *NamespaceLister) GetNamespaces(prefix string, closure bool) ([]string, error) {
+ if prefix == "" {
+ return []string{""}, nil
+ }
+ return nil, nil
+}
diff --git a/api/oci/extensions/repositories/artifactset/state.go b/api/oci/extensions/repositories/artifactset/state.go
new file mode 100644
index 000000000..8901e4684
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/state.go
@@ -0,0 +1,15 @@
+package artifactset
+
+import (
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+// NewStateHandler implements the factory interface for the artifact set
+// state descriptor handling
+// Basically this is an index state.
+func NewStateHandler(fs vfs.FileSystem) accessobj.StateHandler {
+ return &cpi.IndexStateHandler{}
+}
diff --git a/pkg/contexts/oci/repositories/artifactset/suite_test.go b/api/oci/extensions/repositories/artifactset/suite_test.go
similarity index 100%
rename from pkg/contexts/oci/repositories/artifactset/suite_test.go
rename to api/oci/extensions/repositories/artifactset/suite_test.go
diff --git a/pkg/contexts/oci/repositories/artifactset/testhelper/formats.go b/api/oci/extensions/repositories/artifactset/testhelper/formats.go
similarity index 86%
rename from pkg/contexts/oci/repositories/artifactset/testhelper/formats.go
rename to api/oci/extensions/repositories/artifactset/testhelper/formats.go
index 3ac44a18b..3a4ad0922 100644
--- a/pkg/contexts/oci/repositories/artifactset/testhelper/formats.go
+++ b/api/oci/extensions/repositories/artifactset/testhelper/formats.go
@@ -5,7 +5,7 @@ import (
. "github.com/onsi/ginkgo/v2"
- "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
)
func TestForAllFormats(msg string, f func(fmt string)) {
diff --git a/api/oci/extensions/repositories/artifactset/type.go b/api/oci/extensions/repositories/artifactset/type.go
new file mode 100644
index 000000000..512c9bc36
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/type.go
@@ -0,0 +1,88 @@
+package artifactset
+
+import (
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "ArtifactSet"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1))
+}
+
+const (
+ FORMAT_OCI = "oci/v1"
+ FORMAT_OCM = "ocm/v1"
+)
+
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Options `json:",inline"`
+
+ // FileFormat is the format of the repository file
+ FilePath string `json:"filePath"`
+ // AccessMode can be set to request readonly access or creation
+ AccessMode accessobj.AccessMode `json:"accessMode,omitempty"`
+
+ FormatVersion string `json:"formatVersion,omitempty"`
+}
+
+// NewRepositorySpec creates a new RepositorySpec.
+func NewRepositorySpec(acc accessobj.AccessMode, filePath string, opts ...accessio.Option) (*RepositorySpec, error) {
+ o, err := accessio.AccessOptions(&Options{}, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ FilePath: filePath,
+ Options: *o.(*Options),
+ AccessMode: acc,
+ }, nil
+}
+
+func (s *RepositorySpec) Name() string {
+ return s.FilePath
+}
+
+func (s *RepositorySpec) GetFormatVersion() string {
+ if s.FormatVersion == "" {
+ return FORMAT_OCM
+ }
+ return s.FormatVersion
+}
+
+func (s *RepositorySpec) UniformRepositorySpec() *cpi.UniformRepositorySpec {
+ u := &cpi.UniformRepositorySpec{
+ Type: Type,
+ Info: s.FilePath,
+ }
+ return u
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentials) (cpi.Repository, error) {
+ return NewRepository(ctx, a)
+}
+
+func (a *RepositorySpec) AsUniformSpec(cpi.Context) cpi.UniformRepositorySpec {
+ opts, _ := NewOptions(&a.Options) // now unknown option possible (same Options type)
+ p, err := vfs.Canonical(opts.GetPathFileSystem(), a.FilePath, false)
+ if err != nil {
+ return cpi.UniformRepositorySpec{Type: a.GetKind(), Info: a.FilePath}
+ }
+ return cpi.UniformRepositorySpec{Type: a.GetKind(), Info: p}
+}
diff --git a/api/oci/extensions/repositories/artifactset/uniform.go b/api/oci/extensions/repositories/artifactset/uniform.go
new file mode 100644
index 000000000..4711617f4
--- /dev/null
+++ b/api/oci/extensions/repositories/artifactset/uniform.go
@@ -0,0 +1,49 @@
+package artifactset
+
+import (
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+func init() {
+ h := &repospechandler{}
+ cpi.RegisterRepositorySpecHandler(h, "")
+ cpi.RegisterRepositorySpecHandler(h, Type)
+}
+
+type repospechandler struct{}
+
+func (h *repospechandler) MapReference(ctx cpi.Context, u *cpi.UniformRepositorySpec) (cpi.RepositorySpec, error) {
+ path := u.Info
+ if u.Info == "" {
+ if u.Host == "" || u.Type == "" {
+ return nil, nil
+ }
+ path = u.Host
+ }
+ fs := vfsattr.Get(ctx)
+
+ hint, f := accessobj.MapType(u.TypeHint, Type, accessio.FormatDirectory, false)
+ if !u.CreateIfMissing {
+ hint = ""
+ }
+
+ create, ok, err := accessobj.CheckFile(Type, hint, accessio.TypeForTypeSpec(u.Type) == Type, path, fs, ArtifactSetDescriptorFileName)
+ if err == nil && !ok {
+ create, ok, err = accessobj.CheckFile(Type, hint, accessio.TypeForTypeSpec(u.Type) == Type, path, fs, OCIArtifactSetDescriptorFileName)
+ }
+
+ if !ok || err != nil {
+ return nil, err
+ }
+
+ mode := accessobj.ACC_WRITABLE
+ createHint := accessio.FormatNone
+ if create {
+ mode |= accessobj.ACC_CREATE
+ createHint = f
+ }
+ return NewRepositorySpec(mode, path, createHint, accessio.PathFileSystem(fs))
+}
diff --git a/pkg/contexts/oci/repositories/artifactset/utils_synthesis.go b/api/oci/extensions/repositories/artifactset/utils_synthesis.go
similarity index 90%
rename from pkg/contexts/oci/repositories/artifactset/utils_synthesis.go
rename to api/oci/extensions/repositories/artifactset/utils_synthesis.go
index 17db6a721..2a6bd5641 100644
--- a/pkg/contexts/oci/repositories/artifactset/utils_synthesis.go
+++ b/api/oci/extensions/repositories/artifactset/utils_synthesis.go
@@ -9,14 +9,14 @@ import (
"github.com/mandelsoft/vfs/pkg/vfs"
"github.com/opencontainers/go-digest"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/blobaccess/file"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc"
- "github.com/open-component-model/ocm/pkg/contexts/oci/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/oci/transfer"
- "github.com/open-component-model/ocm/pkg/contexts/oci/transfer/filters"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/tools/transfer"
+ "ocm.software/ocm/api/oci/tools/transfer/filters"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/blobaccess/file"
)
const SynthesizedBlobFormat = "+tar+gzip"
diff --git a/pkg/contexts/oci/repositories/ctf/README.md b/api/oci/extensions/repositories/ctf/README.md
similarity index 100%
rename from pkg/contexts/oci/repositories/ctf/README.md
rename to api/oci/extensions/repositories/ctf/README.md
diff --git a/api/oci/extensions/repositories/ctf/ctf_test.go b/api/oci/extensions/repositories/ctf/ctf_test.go
new file mode 100644
index 000000000..405bad923
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/ctf_test.go
@@ -0,0 +1,195 @@
+package ctf_test
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "io"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/oci/extensions/repositories/ctf/testhelper"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/finalizer"
+ "github.com/mandelsoft/logging"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess"
+ ocmlog "ocm.software/ocm/api/utils/logging"
+ "ocm.software/ocm/api/utils/refmgmt"
+)
+
+var _ = Describe("ctf management", func() {
+ var tempfs vfs.FileSystem
+
+ var spec *ctf.RepositorySpec
+
+ ocmlog.Context().AddRule(logging.NewConditionRule(logging.TraceLevel, refmgmt.ALLOC_REALM))
+
+ BeforeEach(func() {
+ t, err := osfs.NewTempFileSystem()
+ Expect(err).To(Succeed())
+ tempfs = t
+
+ spec, err = ctf.NewRepositorySpec(accessobj.ACC_CREATE, "test", accessio.PathFileSystem(tempfs), accessobj.FormatDirectory)
+ Expect(err).To(Succeed())
+ })
+
+ AfterEach(func() {
+ vfs.Cleanup(tempfs)
+ })
+
+ It("instantiate filesystem ctf", func() {
+ var finalize finalizer.Finalizer
+ defer Defer(finalize.Finalize)
+
+ r := Must(ctf.FormatDirectory.Create(oci.DefaultContext(), "test", &spec.StandardOptions, 0o700))
+ finalize.Close(r)
+ Expect(vfs.DirExists(tempfs, "test/"+ctf.BlobsDirectoryName)).To(BeTrue())
+
+ sub := finalize.Nested()
+ n := Must(r.LookupNamespace("mandelsoft/test"))
+ sub.Close(n)
+ DefaultManifestFill(n)
+ Expect(sub.Finalize()).To(Succeed())
+
+ Expect(r.ExistsArtifact("mandelsoft/test", TAG)).To(BeTrue())
+
+ art := Must(r.LookupArtifact("mandelsoft/test", TAG))
+ Close(art, "art")
+
+ Expect(finalize.Finalize()).To(Succeed())
+
+ Expect(vfs.FileExists(tempfs, "test/"+ctf.ArtifactIndexFileName)).To(BeTrue())
+
+ infos, err := vfs.ReadDir(tempfs, "test/"+artifactset.BlobsDirectoryName)
+ Expect(err).To(Succeed())
+ blobs := []string{}
+ for _, fi := range infos {
+ blobs = append(blobs, fi.Name())
+ }
+ Expect(blobs).To(ContainElements(
+ "sha256."+DIGEST_MANIFEST,
+ "sha256."+DIGEST_CONFIG,
+ "sha256."+DIGEST_LAYER))
+ })
+
+ It("instantiate filesystem ctf", func() {
+ r, err := spec.Repository(nil, nil)
+ Expect(err).To(Succeed())
+ Expect(vfs.DirExists(tempfs, "test/"+ctf.BlobsDirectoryName)).To(BeTrue())
+
+ n, err := r.LookupNamespace("mandelsoft/test")
+ Expect(err).To(Succeed())
+ DefaultManifestFill(n)
+
+ Expect(n.Close()).To(Succeed())
+ Expect(r.Close()).To(Succeed())
+ Expect(vfs.FileExists(tempfs, "test/"+ctf.ArtifactIndexFileName)).To(BeTrue())
+
+ infos, err := vfs.ReadDir(tempfs, "test/"+artifactset.BlobsDirectoryName)
+ Expect(err).To(Succeed())
+ blobs := []string{}
+ for _, fi := range infos {
+ blobs = append(blobs, fi.Name())
+ }
+ Expect(blobs).To(ContainElements(
+ "sha256."+DIGEST_MANIFEST,
+ "sha256."+DIGEST_CONFIG,
+ "sha256."+DIGEST_LAYER))
+ })
+
+ It("instantiate tgz artifact", func() {
+ ctf.FormatTGZ.ApplyOption(&spec.StandardOptions)
+ spec.FilePath = "test.tgz"
+ r, err := spec.Repository(nil, nil)
+ Expect(err).To(Succeed())
+
+ n, err := r.LookupNamespace("mandelsoft/test")
+ Expect(err).To(Succeed())
+ DefaultManifestFill(n)
+
+ Expect(n.Close()).To(Succeed())
+ Expect(r.Close()).To(Succeed())
+ Expect(vfs.FileExists(tempfs, "test.tgz")).To(BeTrue())
+
+ file, err := tempfs.Open("test.tgz")
+ Expect(err).To(Succeed())
+ defer file.Close()
+ zip, err := gzip.NewReader(file)
+ Expect(err).To(Succeed())
+ defer zip.Close()
+ tr := tar.NewReader(zip)
+
+ files := []string{}
+ for {
+ header, err := tr.Next()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ Fail(err.Error())
+ }
+
+ switch header.Typeflag {
+ case tar.TypeDir:
+ Expect(header.Name).To(Equal(artifactset.BlobsDirectoryName))
+ case tar.TypeReg:
+ files = append(files, header.Name)
+ }
+ }
+ Expect(files).To(ContainElements(
+ ctf.ArtifactIndexFileName,
+ "blobs/sha256."+DIGEST_MANIFEST,
+ "blobs/sha256."+DIGEST_CONFIG,
+ "blobs/sha256."+DIGEST_LAYER))
+ })
+
+ Context("manifest", func() {
+ It("read from filesystem ctf", func() {
+ r, err := spec.Repository(nil, nil)
+ Expect(err).To(Succeed())
+ Expect(vfs.DirExists(tempfs, "test/"+ctf.BlobsDirectoryName)).To(BeTrue())
+ n, err := r.LookupNamespace("mandelsoft/test")
+ Expect(err).To(Succeed())
+ DefaultManifestFill(n)
+ Expect(n.Close()).To(Succeed())
+ Expect(r.Close()).To(Succeed())
+
+ r, err = ctf.Open(nil, accessobj.ACC_READONLY, "test", 0, accessio.PathFileSystem(tempfs))
+ Expect(err).To(Succeed())
+ defer r.Close()
+
+ n, err = r.LookupNamespace("mandelsoft/test")
+ Expect(err).To(Succeed())
+
+ art, err := n.GetArtifact("sha256:" + DIGEST_MANIFEST)
+ Expect(err).To(Succeed())
+ CheckArtifact(art)
+ art, err = n.GetArtifact(TAG)
+ Expect(err).To(Succeed())
+ b, err := art.GetDescriptor().ToBlobAccess()
+ Expect(err).To(Succeed())
+ Expect(b.Digest()).To(Equal(digest.Digest("sha256:" + DIGEST_MANIFEST)))
+
+ _, err = n.GetArtifact("dummy")
+ Expect(err).To(Equal(errors.ErrNotFound(cpi.KIND_OCIARTIFACT, "dummy", "mandelsoft/test")))
+
+ Expect(n.AddBlob(blobaccess.ForString("", "dummy"))).To(Equal(accessobj.ErrReadOnly))
+
+ n, err = r.LookupNamespace("mandelsoft/other")
+ Expect(err).To(Succeed())
+ _, err = n.GetArtifact("sha256:" + DIGEST_MANIFEST)
+ Expect(err).To(Equal(errors.ErrNotFound(cpi.KIND_OCIARTIFACT, "sha256:"+DIGEST_MANIFEST, "mandelsoft/other")))
+ })
+ })
+})
diff --git a/api/oci/extensions/repositories/ctf/format.go b/api/oci/extensions/repositories/ctf/format.go
new file mode 100644
index 000000000..5143cb2f3
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/format.go
@@ -0,0 +1,171 @@
+package ctf
+
+import (
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf/format"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+const (
+ ArtifactIndexFileName = format.ArtifactIndexFileName
+ BlobsDirectoryName = format.BlobsDirectoryName
+)
+
+var accessObjectInfo = &accessobj.DefaultAccessObjectInfo{
+ DescriptorFileName: ArtifactIndexFileName,
+ ObjectTypeName: "repository",
+ ElementDirectoryName: BlobsDirectoryName,
+ ElementTypeName: "blob",
+ DescriptorHandlerFactory: NewStateHandler,
+}
+
+type Object = Repository
+
+type FormatHandler interface {
+ accessio.Option
+
+ Format() accessio.FileFormat
+
+ Open(ctx cpi.ContextProvider, acc accessobj.AccessMode, path string, opts accessio.Options) (*Object, error)
+ Create(ctx cpi.ContextProvider, path string, opts accessio.Options, mode vfs.FileMode) (*Object, error)
+ Write(obj *Object, path string, opts accessio.Options, mode vfs.FileMode) error
+}
+
+type formatHandler struct {
+ accessobj.FormatHandler
+}
+
+var (
+ FormatDirectory = RegisterFormat(accessobj.FormatDirectory)
+ FormatTAR = RegisterFormat(accessobj.FormatTAR)
+ FormatTGZ = RegisterFormat(accessobj.FormatTGZ)
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+var (
+ fileFormats = map[accessio.FileFormat]FormatHandler{}
+ lock sync.RWMutex
+)
+
+func RegisterFormat(f accessobj.FormatHandler) FormatHandler {
+ lock.Lock()
+ defer lock.Unlock()
+ h := &formatHandler{f}
+ fileFormats[f.Format()] = h
+ return h
+}
+
+func GetFormats() []string {
+ lock.RLock()
+ defer lock.RUnlock()
+ return accessio.GetFormatsFor(fileFormats)
+}
+
+func GetFormat(name accessio.FileFormat) FormatHandler {
+ lock.RLock()
+ defer lock.RUnlock()
+ return fileFormats[name]
+}
+
+func SupportedFormats() []accessio.FileFormat {
+ lock.RLock()
+ defer lock.RUnlock()
+ result := make([]accessio.FileFormat, 0, len(fileFormats))
+ for f := range fileFormats {
+ result = append(result, f)
+ }
+ sort.Slice(result, func(i, j int) bool { return strings.Compare(string(result[i]), string(result[j])) < 0 })
+ return result
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const (
+ ACC_CREATE = accessobj.ACC_CREATE
+ ACC_WRITABLE = accessobj.ACC_WRITABLE
+ ACC_READONLY = accessobj.ACC_READONLY
+)
+
+func OpenFromBlob(ctx cpi.ContextProvider, acc accessobj.AccessMode, blob blobaccess.BlobAccess, opts ...accessio.Option) (*Object, error) {
+ o, err := accessio.AccessOptions(nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if o.GetFile() != nil || o.GetReader() != nil {
+ return nil, errors.ErrInvalid("file or reader option nor possible for blob access")
+ }
+ reader, err := blob.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer reader.Close()
+ o.SetReader(reader)
+ fmt := accessio.FormatTar
+ mime := blob.MimeType()
+ if strings.HasSuffix(mime, "+gzip") {
+ fmt = accessio.FormatTGZ
+ }
+ o.SetFileFormat(fmt)
+ return Open(ctx, acc&accessobj.ACC_READONLY, "", 0, o)
+}
+
+func Open(ctx cpi.ContextProvider, acc accessobj.AccessMode, path string, mode vfs.FileMode, opts ...accessio.Option) (*Object, error) {
+ o, create, err := accessobj.HandleAccessMode(acc, path, nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ h, ok := fileFormats[*o.GetFileFormat()]
+ if !ok {
+ return nil, errors.ErrUnknown(accessobj.KIND_FILEFORMAT, o.GetFileFormat().String())
+ }
+ if create {
+ return h.Create(cpi.FromProvider(ctx), path, o, mode)
+ }
+ return h.Open(cpi.FromProvider(ctx), acc, path, o)
+}
+
+func Create(ctx cpi.ContextProvider, acc accessobj.AccessMode, path string, mode vfs.FileMode, opts ...accessio.Option) (*Object, error) {
+ o, err := accessio.AccessOptions(nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ o.DefaultFormat(accessio.FormatDirectory)
+ h, ok := fileFormats[*o.GetFileFormat()]
+ if !ok {
+ return nil, errors.ErrUnknown(accessobj.KIND_FILEFORMAT, o.GetFileFormat().String())
+ }
+ return h.Create(ctx.OCIContext(), path, o, mode)
+}
+
+func (h *formatHandler) Open(ctx cpi.ContextProvider, acc accessobj.AccessMode, path string, opts accessio.Options) (*Object, error) {
+ obj, err := h.FormatHandler.Open(accessObjectInfo, acc, path, opts)
+ if err != nil {
+ return nil, err
+ }
+ spec, err := NewRepositorySpec(acc, path, opts)
+ return _Wrap(ctx, spec, obj, err)
+}
+
+func (h *formatHandler) Create(ctx cpi.ContextProvider, path string, opts accessio.Options, mode vfs.FileMode) (*Object, error) {
+ obj, err := h.FormatHandler.Create(accessObjectInfo, path, opts, mode)
+ if err != nil {
+ return nil, err
+ }
+ spec, err := NewRepositorySpec(accessobj.ACC_CREATE, path, opts)
+ return _Wrap(ctx, spec, obj, err)
+}
+
+// WriteToFilesystem writes the current object to a filesystem.
+func (h *formatHandler) Write(obj *Object, path string, opts accessio.Options, mode vfs.FileMode) error {
+ return h.FormatHandler.Write(obj.impl.base.Access(), path, opts, mode)
+}
diff --git a/api/oci/extensions/repositories/ctf/format/const.go b/api/oci/extensions/repositories/ctf/format/const.go
new file mode 100644
index 000000000..24a646d9a
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/format/const.go
@@ -0,0 +1,20 @@
+package format
+
+import (
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+const (
+ DirMode = accessobj.DirMode
+ FileMode = accessobj.FileMode
+)
+
+var ModTime = accessobj.ModTime
+
+const (
+ // BlobsDirectoryName is the name of the directory holding the artifact archives.
+ BlobsDirectoryName = artifactset.BlobsDirectoryName
+ // ArtifactIndexFileName is the artifact index descriptor name for CommanTransportFormat.
+ ArtifactIndexFileName = "artifact-index.json"
+)
diff --git a/pkg/contexts/oci/repositories/ctf/formatspec.md b/api/oci/extensions/repositories/ctf/formatspec.md
similarity index 100%
rename from pkg/contexts/oci/repositories/ctf/formatspec.md
rename to api/oci/extensions/repositories/ctf/formatspec.md
diff --git a/pkg/contexts/oci/repositories/ctf/index/ctfindex.go b/api/oci/extensions/repositories/ctf/index/ctfindex.go
similarity index 100%
rename from pkg/contexts/oci/repositories/ctf/index/ctfindex.go
rename to api/oci/extensions/repositories/ctf/index/ctfindex.go
diff --git a/api/oci/extensions/repositories/ctf/index/index.go b/api/oci/extensions/repositories/ctf/index/index.go
new file mode 100644
index 000000000..dabbd888d
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/index/index.go
@@ -0,0 +1,216 @@
+package index
+
+import (
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/image-spec/specs-go"
+
+ "ocm.software/ocm/api/oci/cpi"
+)
+
+type RepositoryIndex struct {
+ lock sync.RWMutex
+ byDigest map[digest.Digest][]*ArtifactMeta
+ byRepository map[string]map[string]*ArtifactMeta
+}
+
+func NewMeta(repo string, tag string, digest digest.Digest) *ArtifactMeta {
+ return &ArtifactMeta{
+ Repository: repo,
+ Tag: tag,
+ Digest: digest,
+ }
+}
+
+func NewRepositoryIndex() *RepositoryIndex {
+ return &RepositoryIndex{
+ byDigest: map[digest.Digest][]*ArtifactMeta{},
+ byRepository: map[string]map[string]*ArtifactMeta{},
+ }
+}
+
+func (r *RepositoryIndex) RepositoryList() []string {
+ result := []string{}
+ for k := range r.byRepository {
+ result = append(result, k)
+ }
+ return result
+}
+
+func (r *RepositoryIndex) AddTagsFor(repo string, digest digest.Digest, tags ...string) error {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+
+ a := r.getArtifactInfo(repo, digest.String())
+ if a == nil {
+ return cpi.ErrUnknownArtifact(repo, digest.String())
+ }
+ for _, tag := range tags {
+ n := *a
+ n.Tag = tag
+ r.addArtifactInfo(n)
+ }
+ return nil
+}
+
+func (r *RepositoryIndex) AddArtifactInfo(n *ArtifactMeta) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ r.addArtifactInfo(*n)
+}
+
+func (r *RepositoryIndex) addArtifactInfo(m ArtifactMeta) {
+ repos := r.byRepository[m.Repository]
+ if len(repos) == 0 {
+ repos = map[string]*ArtifactMeta{}
+ r.byRepository[m.Repository] = repos
+ }
+
+ list := r.byDigest[m.Digest]
+ if list == nil {
+ list = []*ArtifactMeta{&m}
+ } else {
+ for _, e := range list {
+ if m.Repository == e.Repository && m.Digest == e.Digest {
+ if e.Tag == "" || e.Tag == m.Tag {
+ e.Tag = m.Tag
+ if e.Tag != "" {
+ repos[m.Tag] = e
+ }
+ return
+ }
+ }
+ }
+ list = append(list, &m)
+ }
+ r.byDigest[m.Digest] = list
+
+ repos["@"+m.Digest.String()] = &m
+ if m.Tag != "" {
+ repos[m.Tag] = &m
+ }
+}
+
+func (r *RepositoryIndex) HasArtifact(repo, tag string) bool {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ repos := r.byRepository[repo]
+ if repos == nil {
+ return false
+ }
+ m := repos[tag]
+ return m != nil
+}
+
+func (r *RepositoryIndex) GetTags(repo string) []string {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ repos := r.byRepository[repo]
+ if repos == nil {
+ return nil
+ }
+ result := []string{}
+ digests := map[digest.Digest]bool{}
+ for t, a := range repos {
+ if !strings.HasPrefix(t, "@") {
+ result = append(result, t)
+ digests[a.Digest] = true
+ } else if !digests[a.Digest] {
+ digests[a.Digest] = false
+ }
+ }
+ /* TODO: how to query untagged entries at api level
+ for d, found := range digests {
+ if !found {
+ result = append(result, "@"+d.String())
+ }
+ }
+ */
+ return result
+}
+
+func (r *RepositoryIndex) GetArtifacts(repo string) []string {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ repos := r.byRepository[repo]
+ if repos == nil {
+ return nil
+ }
+ result := []string{}
+ for t := range repos {
+ result = append(result, t)
+ }
+ return result
+}
+
+func (r *RepositoryIndex) GetArtifactInfos(digest digest.Digest) []*ArtifactMeta {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ return r.byDigest[digest]
+}
+
+func (r *RepositoryIndex) GetArtifactInfo(repo, reference string) *ArtifactMeta {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ return r.getArtifactInfo(repo, reference)
+}
+
+func (r *RepositoryIndex) getArtifactInfo(repo, reference string) *ArtifactMeta {
+ repos := r.byRepository[repo]
+ if repos == nil {
+ return nil
+ }
+ m := repos[reference]
+ if m == nil && !strings.HasPrefix(reference, "@") {
+ m = repos["@"+reference]
+ }
+ if m == nil {
+ return nil
+ }
+ result := *m
+ return &result
+}
+
+func (r *RepositoryIndex) GetDescriptor() *ArtifactIndex {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ index := &ArtifactIndex{
+ Versioned: specs.Versioned{SchemaVersion},
+ }
+
+ repos := make([]string, len(r.byRepository))
+ i := 0
+ for repo := range r.byRepository {
+ repos[i] = repo
+ i++
+ }
+ sort.Strings(repos)
+ for _, name := range repos {
+ repo := r.byRepository[name]
+ versions := make([]string, len(repo))
+ i := 0
+ for vers := range repo {
+ versions[i] = vers
+ i++
+ }
+ sort.Strings(versions)
+
+ for _, name := range versions {
+ vers := repo[name]
+ if "@"+vers.Digest.String() != name || vers.Tag == "" {
+ d := &ArtifactMeta{
+ Repository: vers.Repository,
+ Tag: vers.Tag,
+ Digest: vers.Digest,
+ }
+ index.Index = append(index.Index, *d)
+ }
+ }
+ }
+ return index
+}
diff --git a/pkg/contexts/oci/repositories/ctf/index/index_test.go b/api/oci/extensions/repositories/ctf/index/index_test.go
similarity index 98%
rename from pkg/contexts/oci/repositories/ctf/index/index_test.go
rename to api/oci/extensions/repositories/ctf/index/index_test.go
index e0a0df01a..9ccfc1a49 100644
--- a/pkg/contexts/oci/repositories/ctf/index/index_test.go
+++ b/api/oci/extensions/repositories/ctf/index/index_test.go
@@ -3,7 +3,7 @@ package index_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf/index"
+ . "ocm.software/ocm/api/oci/extensions/repositories/ctf/index"
)
var _ = Describe("index", func() {
diff --git a/pkg/contexts/oci/repositories/ctf/index/suite_test.go b/api/oci/extensions/repositories/ctf/index/suite_test.go
similarity index 100%
rename from pkg/contexts/oci/repositories/ctf/index/suite_test.go
rename to api/oci/extensions/repositories/ctf/index/suite_test.go
diff --git a/api/oci/extensions/repositories/ctf/namespace.go b/api/oci/extensions/repositories/ctf/namespace.go
new file mode 100644
index 000000000..ac57fd3cb
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/namespace.go
@@ -0,0 +1,100 @@
+package ctf
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/cpi/support"
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf/index"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+func NewNamespace(repo *RepositoryImpl, name string) (cpi.NamespaceAccess, error) {
+ return support.NewNamespaceAccess(name, newNamespaceContainer(repo), repo, "CTF namespace")
+}
+
+type namespaceContainer struct {
+ impl support.NamespaceAccessImpl
+ repo *RepositoryImpl
+}
+
+var _ support.NamespaceContainer = (*namespaceContainer)(nil)
+
+func newNamespaceContainer(repo *RepositoryImpl) support.NamespaceContainer {
+ return &namespaceContainer{
+ repo: repo,
+ }
+}
+
+func (n *namespaceContainer) SetImplementation(impl support.NamespaceAccessImpl) {
+ n.impl = impl
+}
+
+func (n *namespaceContainer) IsReadOnly() bool {
+ return n.repo.IsReadOnly()
+}
+
+func (n *namespaceContainer) Close() error {
+ return nil
+}
+
+func (n *namespaceContainer) GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor {
+ return nil
+}
+
+func (n *namespaceContainer) ListTags() ([]string, error) {
+ return n.repo.getIndex().GetTags(n.impl.GetNamespace()), nil // return digests as tags, also
+}
+
+func (n *namespaceContainer) GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) {
+ return n.repo.base.GetBlobData(digest)
+}
+
+func (n *namespaceContainer) AddBlob(blob cpi.BlobAccess) error {
+ n.repo.base.Lock()
+ defer n.repo.base.Unlock()
+
+ return n.repo.base.AddBlob(blob)
+}
+
+func (n *namespaceContainer) GetArtifact(i support.NamespaceAccessImpl, vers string) (cpi.ArtifactAccess, error) {
+ meta := n.repo.getIndex().GetArtifactInfo(n.impl.GetNamespace(), vers)
+ if meta == nil {
+ return nil, errors.ErrNotFound(cpi.KIND_OCIARTIFACT, vers, n.impl.GetNamespace())
+ }
+ return n.repo.base.GetArtifact(i, meta.Digest)
+}
+
+func (n *namespaceContainer) HasArtifact(vers string) (bool, error) {
+ meta := n.repo.getIndex().GetArtifactInfo(n.impl.GetNamespace(), vers)
+ return meta != nil, nil
+}
+
+func (n *namespaceContainer) AddArtifact(artifact cpi.Artifact, tags ...string) (access blobaccess.BlobAccess, err error) {
+ n.repo.base.Lock()
+ defer n.repo.base.Unlock()
+
+ blob, err := n.repo.base.AddArtifactBlob(artifact)
+ if err != nil {
+ return nil, err
+ }
+ n.repo.getIndex().AddArtifactInfo(&index.ArtifactMeta{
+ Repository: n.impl.GetNamespace(),
+ Tag: "",
+ Digest: blob.Digest(),
+ })
+ return blob, n.AddTags(blob.Digest(), tags...)
+}
+
+func (n *namespaceContainer) AddTags(digest digest.Digest, tags ...string) error {
+ return n.repo.getIndex().AddTagsFor(n.impl.GetNamespace(), digest, tags...)
+}
+
+func (n *namespaceContainer) NewArtifact(i support.NamespaceAccessImpl, art ...cpi.Artifact) (cpi.ArtifactAccess, error) {
+ if n.IsReadOnly() {
+ return nil, accessio.ErrReadOnly
+ }
+ return support.NewArtifact(i, art...)
+}
diff --git a/api/oci/extensions/repositories/ctf/repository.go b/api/oci/extensions/repositories/ctf/repository.go
new file mode 100644
index 000000000..6ec2b0a89
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/repository.go
@@ -0,0 +1,143 @@
+package ctf
+
+import (
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf/index"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/refmgmt"
+)
+
+/*
+ A common transport archive is just a folder with artifact archives.
+ in tar format and an index.json file. The name of the archive
+ is the digest of the artifact descriptor.
+
+ The artifact archive is a filesystem structure with a file
+ artifact-descriptor.json and a folder blobs containing
+ the flat blob files with the name according to the blob digest.
+
+ Digests used as filename will replace the ":" by a "."
+*/
+
+type Repository struct {
+ cpi.Repository
+ impl *RepositoryImpl
+}
+
+func (r *Repository) Write(path string, mode vfs.FileMode, opts ...accessio.Option) error {
+ if r.IsClosed() {
+ return cpi.ErrClosed
+ }
+ return r.impl.Write(path, mode, opts...)
+}
+
+func (r *Repository) Close() error { // why ???
+ return r.Repository.Close()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// RepositoryImpl is closed, if all views are released.
+type RepositoryImpl struct {
+ cpi.RepositoryImplBase
+
+ spec *RepositorySpec
+ base *artifactset.FileSystemBlobAccess
+}
+
+var _ cpi.RepositoryImpl = (*RepositoryImpl)(nil)
+
+// New returns a new representation based repository.
+func New(ctx cpi.Context, spec *RepositorySpec, setup accessobj.Setup, closer accessobj.Closer, mode vfs.FileMode) (*Repository, error) {
+ if spec.GetPathFileSystem() == nil {
+ spec.SetPathFileSystem(vfsattr.Get(ctx))
+ }
+ base, err := accessobj.NewAccessObject(accessObjectInfo, spec.AccessMode, spec.GetRepresentation(), setup, closer, mode)
+ return _Wrap(ctx, spec, base, err)
+}
+
+func _Wrap(ctx cpi.ContextProvider, spec *RepositorySpec, obj *accessobj.AccessObject, err error) (*Repository, error) {
+ if err != nil {
+ return nil, err
+ }
+ impl := &RepositoryImpl{
+ RepositoryImplBase: cpi.NewRepositoryImplBase(cpi.FromProvider(ctx)),
+ spec: spec,
+ base: artifactset.NewFileSystemBlobAccess(obj),
+ }
+ r := cpi.NewRepository(impl, "OCI CTF")
+ return &Repository{r, impl}, nil
+}
+
+func (r *RepositoryImpl) GetSpecification() cpi.RepositorySpec {
+ return r.spec
+}
+
+func (r *RepositoryImpl) NamespaceLister() cpi.NamespaceLister {
+ return r
+}
+
+func (r *RepositoryImpl) NumNamespaces(prefix string) (int, error) {
+ return len(cpi.FilterByNamespacePrefix(prefix, r.getIndex().RepositoryList())), nil
+}
+
+func (r *RepositoryImpl) GetNamespaces(prefix string, closure bool) ([]string, error) {
+ return cpi.FilterChildren(closure, prefix, r.getIndex().RepositoryList()), nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// forward
+
+func (r *RepositoryImpl) IsReadOnly() bool {
+ return r.base.IsReadOnly()
+}
+
+func (r *RepositoryImpl) Write(path string, mode vfs.FileMode, opts ...accessio.Option) error {
+ return r.base.Write(path, mode, opts...)
+}
+
+func (r *RepositoryImpl) Update() error {
+ return r.base.Update()
+}
+
+func (r *RepositoryImpl) Close() error {
+ return r.base.Close()
+}
+
+func (a *RepositoryImpl) getIndex() *index.RepositoryIndex {
+ if a.IsReadOnly() {
+ return a.base.GetState().GetOriginalState().(*index.RepositoryIndex)
+ }
+ return a.base.GetState().GetState().(*index.RepositoryIndex)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// cpi.Repository methods
+
+func (r *RepositoryImpl) ExistsArtifact(name string, tag string) (bool, error) {
+ return r.getIndex().HasArtifact(name, tag), nil
+}
+
+func (r *RepositoryImpl) LookupArtifact(name string, ref string) (acc cpi.ArtifactAccess, err error) {
+ ns, err := NewNamespace(r, name)
+ if err != nil {
+ return nil, err
+ }
+
+ defer refmgmt.PropagateCloseTemporary(&err, ns) // temporary namespace object not exposed.
+
+ a := r.getIndex().GetArtifactInfo(name, ref)
+ if a == nil {
+ return nil, cpi.ErrUnknownArtifact(name, ref)
+ }
+ return ns.GetArtifact(ref)
+}
+
+func (r *RepositoryImpl) LookupNamespace(name string) (cpi.NamespaceAccess, error) {
+ return NewNamespace(r, name)
+}
diff --git a/api/oci/extensions/repositories/ctf/state.go b/api/oci/extensions/repositories/ctf/state.go
new file mode 100644
index 000000000..034bd66f7
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/state.go
@@ -0,0 +1,47 @@
+package ctf
+
+import (
+ "fmt"
+ "reflect"
+
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf/index"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+type StateHandler struct{}
+
+var _ accessobj.StateHandler = &StateHandler{}
+
+func NewStateHandler(fs vfs.FileSystem) accessobj.StateHandler {
+ return &StateHandler{}
+}
+
+func (i StateHandler) Initial() interface{} {
+ return index.NewRepositoryIndex()
+}
+
+func (i StateHandler) Encode(d interface{}) ([]byte, error) {
+ return index.Encode(d.(*index.RepositoryIndex).GetDescriptor())
+}
+
+func (i StateHandler) Decode(data []byte) (interface{}, error) {
+ idx, err := index.Decode(data)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse artifact index read from %s: %w", ArtifactIndexFileName, err)
+ }
+ if idx.SchemaVersion != index.SchemaVersion {
+ return nil, fmt.Errorf("unknown schema version %d for artifact index %s", index.SchemaVersion, ArtifactIndexFileName)
+ }
+
+ artifacts := index.NewRepositoryIndex()
+ for i := range idx.Index {
+ artifacts.AddArtifactInfo(&idx.Index[i])
+ }
+ return artifacts, nil
+}
+
+func (i StateHandler) Equivalent(a, b interface{}) bool {
+ return reflect.DeepEqual(a, b)
+}
diff --git a/pkg/contexts/oci/repositories/ctf/suite_test.go b/api/oci/extensions/repositories/ctf/suite_test.go
similarity index 100%
rename from pkg/contexts/oci/repositories/ctf/suite_test.go
rename to api/oci/extensions/repositories/ctf/suite_test.go
diff --git a/pkg/contexts/oci/repositories/ctf/synthesis_test.go b/api/oci/extensions/repositories/ctf/synthesis_test.go
similarity index 80%
rename from pkg/contexts/oci/repositories/ctf/synthesis_test.go
rename to api/oci/extensions/repositories/ctf/synthesis_test.go
index 11ac47c36..6b636c004 100644
--- a/pkg/contexts/oci/repositories/ctf/synthesis_test.go
+++ b/api/oci/extensions/repositories/ctf/synthesis_test.go
@@ -4,7 +4,7 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf/testhelper"
+ . "ocm.software/ocm/api/oci/extensions/repositories/ctf/testhelper"
"github.com/mandelsoft/filepath/pkg/filepath"
"github.com/mandelsoft/goutils/finalizer"
@@ -12,21 +12,21 @@ import (
"github.com/mandelsoft/vfs/pkg/vfs"
"github.com/opencontainers/go-digest"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/oci"
- "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc"
- "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset"
- "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/localblob"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/digester/digesters/artifact"
- "github.com/open-component-model/ocm/pkg/mime"
- "github.com/open-component-model/ocm/pkg/signing"
- "github.com/open-component-model/ocm/pkg/signing/hasher/sha256"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/digester/digesters/artifact"
+ "ocm.software/ocm/api/tech/signing"
+ "ocm.software/ocm/api/tech/signing/hasher/sha256"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
)
type dummyMethod struct {
diff --git a/pkg/contexts/oci/repositories/ctf/testhelper/fill.go b/api/oci/extensions/repositories/ctf/testhelper/fill.go
similarity index 86%
rename from pkg/contexts/oci/repositories/ctf/testhelper/fill.go
rename to api/oci/extensions/repositories/ctf/testhelper/fill.go
index 653994e27..f19c0f14f 100644
--- a/pkg/contexts/oci/repositories/ctf/testhelper/fill.go
+++ b/api/oci/extensions/repositories/ctf/testhelper/fill.go
@@ -7,11 +7,11 @@ import (
"github.com/mandelsoft/goutils/testutils"
"github.com/opencontainers/go-digest"
- "github.com/open-component-model/ocm/pkg/blobaccess"
- "github.com/open-component-model/ocm/pkg/contexts/oci"
- "github.com/open-component-model/ocm/pkg/contexts/oci/artdesc"
- "github.com/open-component-model/ocm/pkg/contexts/oci/cpi"
- "github.com/open-component-model/ocm/pkg/mime"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
)
const (
diff --git a/api/oci/extensions/repositories/ctf/type.go b/api/oci/extensions/repositories/ctf/type.go
new file mode 100644
index 000000000..433442212
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/type.go
@@ -0,0 +1,83 @@
+package ctf
+
+import (
+ "strings"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = cpi.CommonTransportFormat
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](Type))
+ cpi.RegisterRepositoryType(cpi.NewRepositoryType[*RepositorySpec](TypeV1))
+}
+
+// RepositorySpec describes an OCI registry interface backed by an oci registry.
+type RepositorySpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ accessio.StandardOptions `json:",inline"`
+
+ // FilePath is the file for the repository in the filesystem..
+ FilePath string `json:"filePath"`
+ // AccessMode can be set to request readonly access or creation
+ AccessMode accessobj.AccessMode `json:"accessMode,omitempty"`
+}
+
+var _ cpi.RepositorySpec = (*RepositorySpec)(nil)
+
+var _ cpi.IntermediateRepositorySpecAspect = (*RepositorySpec)(nil)
+
+// NewRepositorySpec creates a new RepositorySpec.
+func NewRepositorySpec(mode accessobj.AccessMode, filePath string, opts ...accessio.Option) (*RepositorySpec, error) {
+ o, err := accessio.AccessOptions(nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if o.GetFileFormat() == nil {
+ for _, v := range SupportedFormats() {
+ if strings.HasSuffix(filePath, "."+v.String()) {
+ o.SetFileFormat(v)
+ break
+ }
+ }
+ }
+ o.Default()
+ return &RepositorySpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ FilePath: filePath,
+ StandardOptions: *o.(*accessio.StandardOptions),
+ AccessMode: mode,
+ }, nil
+}
+
+func (a *RepositorySpec) IsIntermediate() bool {
+ return true
+}
+
+func (a *RepositorySpec) GetType() string {
+ return Type
+}
+
+func (s *RepositorySpec) Name() string {
+ return s.FilePath
+}
+
+func (s *RepositorySpec) UniformRepositorySpec() *cpi.UniformRepositorySpec {
+ u := &cpi.UniformRepositorySpec{
+ Type: Type,
+ Info: s.FilePath,
+ }
+ return u
+}
+
+func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentials) (cpi.Repository, error) {
+ return Open(ctx, a.AccessMode, a.FilePath, 0o700, &a.StandardOptions)
+}
diff --git a/api/oci/extensions/repositories/ctf/uniform.go b/api/oci/extensions/repositories/ctf/uniform.go
new file mode 100644
index 000000000..d351c14fe
--- /dev/null
+++ b/api/oci/extensions/repositories/ctf/uniform.go
@@ -0,0 +1,68 @@
+package ctf
+
+import (
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+const AltType = "ctf"
+
+func init() {
+ h := &repospechandler{}
+ cpi.RegisterRepositorySpecHandler(h, "")
+ cpi.RegisterRepositorySpecHandler(h, Type)
+ cpi.RegisterRepositorySpecHandler(h, AltType)
+ for _, f := range SupportedFormats() {
+ cpi.RegisterRepositorySpecHandler(h, string(f))
+ }
+}
+
+type repospechandler struct{}
+
+func (h *repospechandler) MapReference(ctx cpi.Context, u *cpi.UniformRepositorySpec) (cpi.RepositorySpec, error) {
+ return MapReference(ctx, u)
+}
+
+func explicit(t string) bool {
+ for _, f := range SupportedFormats() {
+ if t == string(f) {
+ return true
+ }
+ }
+ return t == Type || t == AltType
+}
+
+func MapReference(ctx cpi.Context, u *cpi.UniformRepositorySpec) (cpi.RepositorySpec, error) {
+ path := u.Info
+ if u.Info == "" {
+ if u.Host == "" || u.Type == "" {
+ return nil, nil
+ }
+ path = u.Host
+ }
+ fs := vfsattr.Get(ctx)
+
+ typ, _ := accessobj.MapType(u.Type, Type, accessio.FormatNone, true, AltType)
+ hint, f := accessobj.MapType(u.TypeHint, Type, accessio.FormatDirectory, true, AltType)
+ if !u.CreateIfMissing {
+ hint = ""
+ }
+ create, ok, err := accessobj.CheckFile(Type, hint, explicit(accessio.TypeForTypeSpec(u.Type)), path, fs, ArtifactIndexFileName)
+ if !ok || (err != nil && typ == "") {
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return nil, nil
+ }
+ }
+ mode := accessobj.ACC_WRITABLE
+ createHint := accessio.FormatNone
+ if create {
+ mode |= accessobj.ACC_CREATE
+ createHint = f
+ }
+ return NewRepositorySpec(mode, path, createHint, accessio.PathFileSystem(fs))
+}
diff --git a/pkg/contexts/oci/repositories/docker/README.md b/api/oci/extensions/repositories/docker/README.md
similarity index 100%
rename from pkg/contexts/oci/repositories/docker/README.md
rename to api/oci/extensions/repositories/docker/README.md
diff --git a/api/oci/extensions/repositories/docker/artifact.go b/api/oci/extensions/repositories/docker/artifact.go
new file mode 100644
index 000000000..b1b5567ca
--- /dev/null
+++ b/api/oci/extensions/repositories/docker/artifact.go
@@ -0,0 +1,69 @@
+package docker
+
+import (
+ "sync"
+
+ "github.com/containers/image/v5/types"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+type dockerSource struct {
+ lock sync.RWMutex
+ src types.ImageSource
+ img types.Image
+ refcount int
+}
+
+var _ accessio.BlobSource = (*dockerSource)(nil)
+
+func newDockerSource(img types.Image, src types.ImageSource) *dockerSource {
+ return &dockerSource{
+ src: src,
+ img: img,
+ refcount: 1,
+ }
+}
+
+func (c *dockerSource) Ref() error {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if c.refcount == 0 {
+ return accessio.ErrClosed
+ }
+ c.refcount++
+ return nil
+}
+
+func (c *dockerSource) Unref() error {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if c.refcount == 0 {
+ return accessio.ErrClosed
+ }
+ c.refcount--
+ return c.src.Close()
+}
+
+func (d *dockerSource) GetBlobData(digest digest.Digest) (int64, blobaccess.DataAccess, error) {
+ info := d.img.ConfigInfo()
+ if info.Digest == digest {
+ data, err := d.img.ConfigBlob(dummyContext)
+ if err != nil {
+ return -1, nil, err
+ }
+ return info.Size, blobaccess.DataAccessForData(data), nil
+ }
+ info.Digest = ""
+ for _, l := range d.img.LayerInfos() {
+ if l.Digest == digest {
+ info = l
+ acc, err := NewDataAccess(d.src, info, false)
+ return l.Size, acc, err
+ }
+ }
+ return -1, nil, cpi.ErrBlobNotFound(digest)
+}
diff --git a/pkg/contexts/oci/repositories/docker/client.go b/api/oci/extensions/repositories/docker/client.go
similarity index 100%
rename from pkg/contexts/oci/repositories/docker/client.go
rename to api/oci/extensions/repositories/docker/client.go
diff --git a/api/oci/extensions/repositories/docker/convert.go b/api/oci/extensions/repositories/docker/convert.go
new file mode 100644
index 000000000..7191eb371
--- /dev/null
+++ b/api/oci/extensions/repositories/docker/convert.go
@@ -0,0 +1,203 @@
+package docker
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/containers/image/v5/image"
+ "github.com/containers/image/v5/manifest"
+ "github.com/containers/image/v5/types"
+ "github.com/opencontainers/go-digest"
+ "github.com/sirupsen/logrus"
+
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+// fakeSource implements required methods to call the manifest conversion.
+type fakeSource struct {
+ types.ImageSource
+ art cpi.BlobAccess
+ blobs cpi.BlobSource
+ ref types.ImageReference
+}
+
+func (f *fakeSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
+ if instanceDigest != nil {
+ return nil, "", fmt.Errorf("manifest lists are not supported")
+ }
+ data, err := f.art.Get()
+ if err != nil {
+ return nil, "", err
+ }
+ return data, f.art.MimeType(), nil
+}
+
+func (f *fakeSource) GetBlob(ctx context.Context, bi types.BlobInfo, bc types.BlobInfoCache) (io.ReadCloser, int64, error) {
+ _, blob, err := f.blobs.GetBlobData(bi.Digest)
+ if err != nil {
+ return nil, blobaccess.BLOB_UNKNOWN_SIZE, err
+ }
+
+ r, err := blob.Reader()
+ return r, bi.Size, err
+}
+
+func (f *fakeSource) Reference() types.ImageReference {
+ return f.ref
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type artBlobCache struct {
+ access cpi.ArtifactAccess
+}
+
+var _ accessio.BlobCache = (*artBlobCache)(nil)
+
+func ArtifactAsBlobCache(access cpi.ArtifactAccess) accessio.BlobCache {
+ return &artBlobCache{access}
+}
+
+func (a *artBlobCache) Ref() error {
+ return nil
+}
+
+func (a *artBlobCache) Unref() error {
+ return nil
+}
+
+func (a *artBlobCache) GetBlobData(digest digest.Digest) (int64, blobaccess.DataAccess, error) {
+ blob, err := a.access.GetBlob(digest)
+ if err != nil {
+ return -1, nil, err
+ }
+ return blob.Size(), blob, err
+}
+
+func (a *artBlobCache) AddBlob(blob blobaccess.BlobAccess) (int64, digest.Digest, error) {
+ err := a.access.AddBlob(blob)
+ if err != nil {
+ return -1, "", err
+ }
+ return blob.Size(), blob.Digest(), err
+}
+
+func (c *artBlobCache) AddData(data blobaccess.DataAccess) (int64, digest.Digest, error) {
+ return c.AddBlob(blobaccess.ForDataAccess(blobaccess.BLOB_UNKNOWN_DIGEST, blobaccess.BLOB_UNKNOWN_SIZE, "", data))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func blobSource(art cpi.Artifact, blobs accessio.BlobSource) (accessio.BlobSource, error) {
+ var err error
+ if blobs == nil {
+ if t, ok := art.(cpi.ArtifactAccess); !ok {
+ return nil, fmt.Errorf("blob source required")
+ } else {
+ blobs = ArtifactAsBlobCache(t)
+ }
+ } else {
+ if t, ok := art.(cpi.ArtifactAccess); ok {
+ blobs, err = accessio.NewCascadedBlobCacheForSource(blobs, ArtifactAsBlobCache(t))
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ return blobs, nil
+}
+
+func Convert(art cpi.Artifact, blobs accessio.BlobSource, dst types.ImageDestination) (cpi.BlobAccess, error) {
+ blobs, err := blobSource(art, blobs)
+ if err != nil {
+ return nil, err
+ }
+ artblob, err := art.Blob()
+ if err != nil {
+ return nil, err
+ }
+ ociImage := &fakeSource{
+ art: artblob,
+ blobs: blobs,
+ ref: dst.Reference(),
+ }
+
+ m, err := art.Manifest()
+ if err != nil {
+ return nil, err
+ }
+ for i, l := range m.Layers {
+ size, blob, err := blobs.GetBlobData(l.Digest)
+ if err != nil {
+ return nil, err
+ }
+ r, err := blob.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ bi := types.BlobInfo{
+ Digest: l.Digest,
+ Size: size,
+ URLs: l.URLs,
+ Annotations: l.Annotations,
+ MediaType: l.MediaType,
+ }
+ logrus.Infof("put blob for layer %d", i)
+ _, err = dst.PutBlob(dummyContext, r, bi, nil, false)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ un := image.UnparsedInstance(ociImage, nil)
+ img, err := image.FromUnparsedImage(dummyContext, nil, un)
+ if err != nil {
+ return nil, err
+ }
+
+ opts := types.ManifestUpdateOptions{
+ ManifestMIMEType: manifest.DockerV2Schema2MediaType,
+ InformationOnly: types.ManifestUpdateInformation{
+ Destination: dst,
+ },
+ }
+
+ img, err = img.UpdatedImage(dummyContext, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ bi := img.ConfigInfo()
+ blob, err := img.ConfigBlob(dummyContext)
+ if err != nil {
+ return nil, err
+ }
+ var reader io.ReadCloser
+ if blob == nil {
+ _, orig, err := blobs.GetBlobData(bi.Digest)
+ if err != nil {
+ return nil, err
+ }
+ reader, err = orig.Reader()
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ reader = io.NopCloser(bytes.NewReader(blob))
+ }
+ _, err = dst.PutBlob(dummyContext, reader, bi, nil, true)
+ if err != nil {
+ return nil, err
+ }
+ man, _, err := img.Manifest(dummyContext)
+ if err != nil {
+ return nil, err
+ }
+
+ return artblob, dst.PutManifest(dummyContext, man, nil)
+}
diff --git a/api/oci/extensions/repositories/docker/namespace.go b/api/oci/extensions/repositories/docker/namespace.go
new file mode 100644
index 000000000..2a346cdd1
--- /dev/null
+++ b/api/oci/extensions/repositories/docker/namespace.go
@@ -0,0 +1,278 @@
+package docker
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/containers/image/v5/image"
+ "github.com/containers/image/v5/types"
+ dockertypes "github.com/docker/docker/api/types/image"
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/logging"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/cpi"
+ "ocm.software/ocm/api/oci/cpi/support"
+ "ocm.software/ocm/api/oci/internal"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+type blobHandler struct {
+ accessio.BlobCache
+}
+
+var _ support.BlobProvider = (*blobHandler)(nil)
+
+func newBlobHandler(cache accessio.BlobCache) support.BlobProvider {
+ return &blobHandler{cache}
+}
+
+func (b blobHandler) AddBlob(access internal.BlobAccess) error {
+ _, _, err := b.BlobCache.AddBlob(access)
+ return err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// namespaceContainer delegates functionality but blob access to an underlying
+// handler.
+// blob access is handled locally.
+type namespaceContainer struct {
+ *namespaceHandler
+ blobs support.BlobProvider
+}
+
+var _ support.NamespaceContainer = (*namespaceContainer)(nil)
+
+func newNamespaceContainer(handler *namespaceHandler, blobs support.BlobProvider) *namespaceContainer {
+ return &namespaceContainer{
+ namespaceHandler: handler,
+ blobs: blobs,
+ }
+}
+
+func NewNamespace(repo *RepositoryImpl, name string) (cpi.NamespaceAccess, error) {
+ h, err := newNamespaceHandler(repo)
+ if err != nil {
+ return nil, err
+ }
+ // initial container wrapper releases base cache with close of namespace
+ // container on last namespace ref.
+ // base cache has initial user count of 1.
+ return support.NewNamespaceAccess(name, newNamespaceContainer(h, h.blobs), repo, "docker namespace")
+}
+
+func (n *namespaceContainer) Close() error {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+
+ if n.blobs != nil {
+ err := n.blobs.Unref()
+ n.blobs = nil
+ if err != nil {
+ return fmt.Errorf("failed to unref: %w", err)
+ }
+ }
+ return nil
+}
+
+func (n *namespaceContainer) GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) {
+ return n.blobs.GetBlobData(digest)
+}
+
+func (n *namespaceContainer) AddBlob(blob cpi.BlobAccess) error {
+ if err := n.blobs.AddBlob(blob); err != nil {
+ return fmt.Errorf("failed to add blob to cache: %w", err)
+ }
+
+ return nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type namespaceHandler struct {
+ impl support.NamespaceAccessImpl
+ lock sync.RWMutex
+ repo *RepositoryImpl
+ blobs support.BlobProvider
+ log logging.Logger
+}
+
+func newNamespaceHandler(repo *RepositoryImpl) (*namespaceHandler, error) {
+ cache, err := accessio.NewCascadedBlobCache(nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &namespaceHandler{
+ repo: repo,
+ blobs: newBlobHandler(cache),
+ log: repo.GetContext().Logger(),
+ }, nil
+}
+
+func (n *namespaceHandler) SetImplementation(impl support.NamespaceAccessImpl) {
+ n.impl = impl
+}
+
+func (n *namespaceHandler) IsReadOnly() bool {
+ return n.repo.IsReadOnly()
+}
+
+func (n *namespaceContainer) GetBlobDescriptor(digest digest.Digest) *cpi.Descriptor {
+ return nil
+}
+
+func (n *namespaceHandler) ListTags() ([]string, error) {
+ opts := dockertypes.ListOptions{}
+ list, err := n.repo.client.ImageList(dummyContext, opts)
+ if err != nil {
+ return nil, err
+ }
+ var result []string
+ if n.impl.GetNamespace() == "" {
+ for _, e := range list {
+ // ID is always the config digest
+ // filter images without a repo tag for empty namespace
+ if len(e.RepoTags) == 0 {
+ d, err := digest.Parse(e.ID)
+ if err == nil {
+ result = append(result, d.String()[:12])
+ }
+ }
+ }
+ } else {
+ prefix := n.impl.GetNamespace() + ":"
+ for _, e := range list {
+ for _, t := range e.RepoTags {
+ if strings.HasPrefix(t, prefix) {
+ result = append(result, t[len(prefix):])
+ }
+ }
+ }
+ }
+ return result, nil
+}
+
+func (n *namespaceHandler) GetArtifact(i support.NamespaceAccessImpl, vers string) (cpi.ArtifactAccess, error) {
+ ref, err := ParseRef(n.impl.GetNamespace(), vers)
+ if err != nil {
+ return nil, err
+ }
+ src, err := ref.NewImageSource(dummyContext, n.repo.sysctx)
+ if err != nil {
+ return nil, err
+ }
+
+ opts := types.ManifestUpdateOptions{
+ ManifestMIMEType: artdesc.MediaTypeImageManifest,
+ }
+ un := image.UnparsedInstance(src, nil)
+ img, err := image.FromUnparsedImage(dummyContext, n.repo.sysctx, un)
+ if err != nil {
+ src.Close()
+ return nil, err
+ }
+
+ img, err = img.UpdatedImage(dummyContext, opts)
+ if err != nil {
+ src.Close()
+ return nil, err
+ }
+
+ data, mime, err := img.Manifest(dummyContext)
+ if err != nil {
+ src.Close()
+ return nil, err
+ }
+
+ cache, err := accessio.NewCascadedBlobCacheForSource(n.blobs, newDockerSource(img, src))
+ if err != nil {
+ return nil, err
+ }
+
+ priv := i.WithContainer(newNamespaceContainer(n, newBlobHandler(cache)))
+ // assure explicit close of wrapper container for artifact close
+ return support.NewArtifactForBlob(priv, blobaccess.ForData(mime, data), priv)
+}
+
+func (n *namespaceHandler) HasArtifact(vers string) (bool, error) {
+ list, err := n.ListTags()
+ if err != nil {
+ return false, err
+ }
+ for _, e := range list {
+ if e == vers {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (n *namespaceContainer) AddArtifact(artifact cpi.Artifact, tags ...string) (access blobaccess.BlobAccess, err error) {
+ tag := "latest"
+ if len(tags) > 0 {
+ tag = tags[0]
+ }
+ ref, err := ParseRef(n.impl.GetNamespace(), tag)
+ if err != nil {
+ return nil, err
+ }
+ dst, err := ref.NewImageDestination(dummyContext, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer dst.Close()
+
+ blob, err := Convert(artifact, n.blobs, dst)
+ if err != nil {
+ return nil, err
+ }
+ err = dst.Commit(dummyContext, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return blob, nil
+}
+
+func (n *namespaceContainer) AddTags(digest digest.Digest, tags ...string) error {
+ if ok, _ := artdesc.IsDigest(digest.String()); ok {
+ return errors.ErrNotSupported("image access by digest")
+ }
+
+ src := n.impl.GetNamespace() + ":" + digest.String()
+
+ if pattern.MatchString(digest.String()) {
+ // this definitely no digest, but the library expects it this way
+ src = digest.String()
+ }
+
+ for _, tag := range tags {
+ err := n.repo.client.ImageTag(dummyContext, src, n.impl.GetNamespace()+":"+tag)
+ if err != nil {
+ return fmt.Errorf("failed to add image tag: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func (n *namespaceContainer) NewArtifact(i support.NamespaceAccessImpl, art ...cpi.Artifact) (cpi.ArtifactAccess, error) {
+ if n.IsReadOnly() {
+ return nil, accessio.ErrReadOnly
+ }
+ var m cpi.Artifact
+ if len(art) == 0 || art[0] == nil {
+ m = artdesc.NewManifest()
+ } else {
+ m = art[0]
+ if !m.IsValid() {
+ m = artdesc.NewManifest()
+ }
+ }
+ return support.NewArtifact(i, m)
+}
diff --git a/api/oci/extensions/repositories/docker/repository.go b/api/oci/extensions/repositories/docker/repository.go
new file mode 100644
index 000000000..52836d19b
--- /dev/null
+++ b/api/oci/extensions/repositories/docker/repository.go
@@ -0,0 +1,121 @@
+package docker
+
+import (
+ "strings"
+
+ "github.com/containers/image/v5/types"
+ dockertypes "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/client"
+
+ "ocm.software/ocm/api/oci/cpi"
+)
+
+type RepositoryImpl struct {
+ cpi.RepositoryImplBase
+ spec *RepositorySpec
+ sysctx *types.SystemContext
+ client *client.Client
+}
+
+var _ cpi.RepositoryImpl = (*RepositoryImpl)(nil)
+
+func NewRepository(ctx cpi.Context, spec *RepositorySpec) (cpi.Repository, error) {
+ client, err := newDockerClient(spec.DockerHost)
+ if err != nil {
+ return nil, err
+ }
+
+ sysctx := &types.SystemContext{
+ DockerDaemonHost: client.DaemonHost(),
+ }
+
+ i := &RepositoryImpl{
+ RepositoryImplBase: cpi.NewRepositoryImplBase(ctx),
+ spec: spec,
+ sysctx: sysctx,
+ client: client,
+ }
+ return cpi.NewRepository(i, "docker"), nil
+}
+
+func (r *RepositoryImpl) Close() error {
+ return nil
+}
+
+func (r *RepositoryImpl) IsReadOnly() bool {
+ return true
+}
+
+func (r *RepositoryImpl) GetSpecification() cpi.RepositorySpec {
+ return r.spec
+}
+
+func (r *RepositoryImpl) NamespaceLister() cpi.NamespaceLister {
+ return r
+}
+
+func (r *RepositoryImpl) NumNamespaces(prefix string) (int, error) {
+ repos, err := r.GetRepositories()
+ if err != nil {
+ return -1, err
+ }
+ return len(cpi.FilterByNamespacePrefix(prefix, repos)), nil
+}
+
+func (r *RepositoryImpl) GetNamespaces(prefix string, closure bool) ([]string, error) {
+ repos, err := r.GetRepositories()
+ if err != nil {
+ return nil, err
+ }
+ return cpi.FilterChildren(closure, prefix, repos), nil
+}
+
+func (r *RepositoryImpl) GetRepositories() ([]string, error) {
+ opts := dockertypes.ListOptions{}
+ list, err := r.client.ImageList(dummyContext, opts)
+ if err != nil {
+ return nil, err
+ }
+ var result cpi.StringList
+ for _, e := range list {
+ if len(e.RepoTags) > 0 {
+ for _, t := range e.RepoTags {
+ i := strings.Index(t, ":")
+ if i > 0 {
+ if t[:i] != "` + ConfigType + `
can be used to set some
+configurations for an OCM context;
+
++ type: ` + ConfigType + ` + aliases: + myrepo: + type: <any repository type> + <specification attributes> + ... + resolvers: + - repository: + type: <any repository type> + <specification attributes> + ... + prefix: ghcr.io/open-component-model/ocm + priority: 10 ++ +With aliases repository alias names can be mapped to a repository specification. +The alias name can be used in a string notation for an OCM repository. + +Resolvers define a list of OCM repository specifications to be used to resolve +dedicated component versions. These settings are used to compose a standard +component version resolver provided for an OCM context. Optionally, a component +name prefix can be given. It limits the usage of the repository to resolve only +components with the given name prefix (always complete name segments). +An optional priority can be used to influence the lookup order. Larger value +means higher priority (default 10). + +All matching entries are tried to lookup a component version in the following +order: +- highest priority first +- longest matching sequence of component name segments first. + +If resolvers are defined, it is possible to use component version names on the +command line without a repository. The names are resolved with the specified +resolution rule. +They are also used as default lookup repositories to lookup component references +for recursive operations on component versions (
--lookup
option).
+`
diff --git a/api/ocm/consts/deprecated.go b/api/ocm/consts/deprecated.go
new file mode 100644
index 000000000..07403f4e8
--- /dev/null
+++ b/api/ocm/consts/deprecated.go
@@ -0,0 +1,18 @@
+package consts
+
+import (
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extraid"
+)
+
+const (
+ // Deprecated: use extraid.SystemIdentityName.
+ SystemIdentityName = metav1.SystemIdentityName
+ // Deprecated: use extraid.SystemIdentityVersion .
+ SystemIdentityVersion = metav1.SystemIdentityVersion
+
+ // Deprecated: use extraid.ExecutableOperatingSystem .
+ ExecutableOperatingSystem = extraid.ExecutableOperatingSystem
+ // Deprecated: use extraid.ExecutableArchitecture .
+ ExecutableArchitecture = extraid.ExecutableArchitecture
+)
diff --git a/pkg/contexts/ocm/cpi/README.md b/api/ocm/cpi/README.md
similarity index 100%
rename from pkg/contexts/ocm/cpi/README.md
rename to api/ocm/cpi/README.md
diff --git a/pkg/contexts/ocm/cpi/accspeccpi/accessspec_options.go b/api/ocm/cpi/accspeccpi/accessspec_options.go
similarity index 75%
rename from pkg/contexts/ocm/cpi/accspeccpi/accessspec_options.go
rename to api/ocm/cpi/accspeccpi/accessspec_options.go
index e9d2cd613..c0a4bcbb0 100644
--- a/pkg/contexts/ocm/cpi/accspeccpi/accessspec_options.go
+++ b/api/ocm/cpi/accspeccpi/accessspec_options.go
@@ -1,8 +1,8 @@
package accspeccpi
import (
- "github.com/open-component-model/ocm/pkg/cobrautils/flagsets"
- "github.com/open-component-model/ocm/pkg/cobrautils/flagsets/flagsetscheme"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme"
)
type AccessSpecTypeOption = flagsetscheme.TypeOption
diff --git a/api/ocm/cpi/accspeccpi/accesstypes.go b/api/ocm/cpi/accspeccpi/accesstypes.go
new file mode 100644
index 000000000..e71fcc9b8
--- /dev/null
+++ b/api/ocm/cpi/accspeccpi/accesstypes.go
@@ -0,0 +1,44 @@
+package accspeccpi
+
+import (
+ "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type AccessTypeVersionScheme = runtime.TypeVersionScheme[AccessSpec, AccessType]
+
+func NewAccessTypeVersionScheme(kind string) AccessTypeVersionScheme {
+ return runtime.NewTypeVersionScheme[AccessSpec, AccessType](kind, newStrictAccessTypeScheme())
+}
+
+func RegisterAccessType(atype AccessType) {
+ defaultAccessTypeScheme.Register(atype)
+}
+
+func RegisterAccessTypeVersions(s AccessTypeVersionScheme) {
+ defaultAccessTypeScheme.AddKnownTypes(s)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type AccessSpecFormatVersionRegistry = runtime.FormatVersionRegistry[AccessSpec]
+
+func NewAccessSpecFormatVersionRegistry() AccessSpecFormatVersionRegistry {
+ return runtime.NewFormatVersionRegistry[AccessSpec]()
+}
+
+func MustNewAccessSpecMultiFormatVersion(kind string, formats AccessSpecFormatVersionRegistry) runtime.FormatVersion[AccessSpec] {
+ return runtime.MustNewMultiFormatVersion[AccessSpec](kind, formats)
+}
+
+func NewAccessSpecType[I AccessSpec](name string, opts ...AccessSpecTypeOption) AccessType {
+ return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectType[AccessSpec, I](name), opts...)
+}
+
+func NewAccessSpecTypeByConverter[I AccessSpec, V runtime.VersionedTypedObject](name string, converter runtime.Converter[I, V], opts ...AccessSpecTypeOption) AccessType {
+ return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByConverter[AccessSpec, I, V](name, converter), opts...)
+}
+
+func NewAccessSpecTypeByFormatVersion(name string, fmt runtime.FormatVersion[AccessSpec], opts ...AccessSpecTypeOption) AccessType {
+ return flagsetscheme.NewTypedObjectTypeObject[AccessSpec](runtime.NewVersionedTypedObjectTypeByFormatVersion[AccessSpec](name, fmt), opts...)
+}
diff --git a/api/ocm/cpi/accspeccpi/interface.go b/api/ocm/cpi/accspeccpi/interface.go
new file mode 100644
index 000000000..2334058bd
--- /dev/null
+++ b/api/ocm/cpi/accspeccpi/interface.go
@@ -0,0 +1,33 @@
+package accspeccpi
+
+import (
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/internal"
+)
+
+type (
+ Context = internal.Context
+ ContextProvider = internal.ContextProvider
+
+ AccessType = internal.AccessType
+
+ AccessMethodImpl = internal.AccessMethodImpl
+ AccessMethod = internal.AccessMethod
+ AccessSpec = internal.AccessSpec
+ AccessSpecRef = internal.AccessSpecRef
+
+ HintProvider = internal.HintProvider
+ GlobalAccessProvider = internal.GlobalAccessProvider
+ CosumerIdentityProvider = credentials.ConsumerIdentityProvider
+
+ ComponentVersionAccess = internal.ComponentVersionAccess
+)
+
+var (
+ newStrictAccessTypeScheme = internal.NewStrictAccessTypeScheme
+ defaultAccessTypeScheme = internal.DefaultAccessTypeScheme
+)
+
+func NewAccessSpecRef(spec AccessSpec) *AccessSpecRef {
+ return internal.NewAccessSpecRef(spec)
+}
diff --git a/api/ocm/cpi/accspeccpi/method.go b/api/ocm/cpi/accspeccpi/method.go
new file mode 100644
index 000000000..84f077c00
--- /dev/null
+++ b/api/ocm/cpi/accspeccpi/method.go
@@ -0,0 +1,129 @@
+package accspeccpi
+
+import (
+ "io"
+ "sync"
+
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+type DefaultAccessMethodImpl struct {
+ lock sync.Mutex
+ blob blobaccess.BlobAccess
+
+ factory BlobAccessFactory
+ comp ComponentVersionAccess
+ spec AccessSpec
+ mime string
+ digest digest.Digest
+ local bool
+}
+
+var (
+ _ AccessMethodImpl = (*DefaultAccessMethodImpl)(nil)
+ _ blobaccess.DigestSource = (*DefaultAccessMethodImpl)(nil)
+)
+
+type BlobAccessFactory func() (blobaccess.BlobAccess, error)
+
+func NewDefaultMethod(c ComponentVersionAccess, a AccessSpec, digest digest.Digest, mime string, fac BlobAccessFactory, local ...bool) AccessMethod {
+ m, _ := AccessMethodForImplementation(NewDefaultMethodImpl(c, a, digest, mime, fac, local...), nil)
+ return m
+}
+
+func NewDefaultMethodImpl(c ComponentVersionAccess, a AccessSpec, digest digest.Digest, mime string, fac BlobAccessFactory, local ...bool) AccessMethodImpl {
+ return &DefaultAccessMethodImpl{
+ spec: a,
+ comp: c,
+ mime: mime,
+ digest: digest,
+ factory: fac,
+ local: utils.Optional(local...),
+ }
+}
+
+func NewDefaultMethodForBlobAccess(c ComponentVersionAccess, a AccessSpec, digest digest.Digest, blob blobaccess.BlobAccess, local ...bool) (AccessMethod, error) {
+ return AccessMethodForImplementation(NewDefaultMethodImplForBlobAccess(c, a, digest, blob, local...))
+}
+
+func NewDefaultMethodImplForBlobAccess(c ComponentVersionAccess, a AccessSpec, digest digest.Digest, blob blobaccess.BlobAccess, local ...bool) (AccessMethodImpl, error) {
+ blob, err := blob.Dup()
+ if err != nil {
+ return nil, err
+ }
+ return &DefaultAccessMethodImpl{
+ spec: a,
+ blob: blob,
+ comp: c,
+ mime: blob.MimeType(),
+ digest: digest,
+ factory: nil,
+ local: utils.Optional(local...),
+ }, nil
+}
+
+func (m *DefaultAccessMethodImpl) getAccess() (blobaccess.BlobAccess, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if m.blob == nil {
+ acc, err := m.factory()
+ if err != nil {
+ return nil, err
+ }
+ m.blob = acc
+ }
+ return m.blob, nil
+}
+
+func (m *DefaultAccessMethodImpl) Digest() digest.Digest {
+ return m.digest
+}
+
+func (m *DefaultAccessMethodImpl) IsLocal() bool {
+ return m.local
+}
+
+func (m *DefaultAccessMethodImpl) GetKind() string {
+ return m.spec.GetKind()
+}
+
+func (m *DefaultAccessMethodImpl) AccessSpec() AccessSpec {
+ return m.spec
+}
+
+func (m *DefaultAccessMethodImpl) Get() ([]byte, error) {
+ a, err := m.getAccess()
+ if err != nil {
+ return nil, err
+ }
+ return a.Get()
+}
+
+func (m *DefaultAccessMethodImpl) Reader() (io.ReadCloser, error) {
+ a, err := m.getAccess()
+ if err != nil {
+ return nil, err
+ }
+ return a.Reader()
+}
+
+func (m *DefaultAccessMethodImpl) Close() error {
+ var err error
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil {
+ err = m.blob.Close()
+ m.blob = nil
+ }
+ return err
+}
+
+func (m *DefaultAccessMethodImpl) MimeType() string {
+ return m.mime
+}
diff --git a/pkg/contexts/ocm/cpi/accspeccpi/methodview.go b/api/ocm/cpi/accspeccpi/methodview.go
similarity index 93%
rename from pkg/contexts/ocm/cpi/accspeccpi/methodview.go
rename to api/ocm/cpi/accspeccpi/methodview.go
index 61e93bc8b..670d258f0 100644
--- a/pkg/contexts/ocm/cpi/accspeccpi/methodview.go
+++ b/api/ocm/cpi/accspeccpi/methodview.go
@@ -6,10 +6,10 @@ import (
"github.com/modern-go/reflect2"
"github.com/opencontainers/go-digest"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/contexts/credentials"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/refmgmt"
)
type DigestSource interface {
diff --git a/api/ocm/cpi/builder.go b/api/ocm/cpi/builder.go
new file mode 100644
index 000000000..c858f43b9
--- /dev/null
+++ b/api/ocm/cpi/builder.go
@@ -0,0 +1,50 @@
+package cpi
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm/internal"
+)
+
+func WithContext(ctx context.Context) internal.Builder {
+ return internal.Builder{}.WithContext(ctx)
+}
+
+func WithCredentials(ctx credentials.Context) internal.Builder {
+ return internal.Builder{}.WithCredentials(ctx)
+}
+
+func WithOCIRepositories(ctx oci.Context) internal.Builder {
+ return internal.Builder{}.WithOCIRepositories(ctx)
+}
+
+func WithRepositoyTypeScheme(scheme RepositoryTypeScheme) internal.Builder {
+ return internal.Builder{}.WithRepositoyTypeScheme(scheme)
+}
+
+func WithRepositoryDelegation(reg RepositoryDelegationRegistry) internal.Builder {
+ return internal.Builder{}.WithRepositoryDelegation(reg)
+}
+
+func WithAccessypeScheme(scheme AccessTypeScheme) internal.Builder {
+ return internal.Builder{}.WithAccessTypeScheme(scheme)
+}
+
+func WithRepositorySpecHandlers(reg RepositorySpecHandlers) internal.Builder {
+ return internal.Builder{}.WithRepositorySpecHandlers(reg)
+}
+
+func WithBlobHandlers(reg BlobHandlerRegistry) internal.Builder {
+ return internal.Builder{}.WithBlobHandlers(reg)
+}
+
+func WithBlobDigesters(reg BlobDigesterRegistry) internal.Builder {
+ return internal.Builder{}.WithBlobDigesters(reg)
+}
+
+func New(mode ...datacontext.BuilderMode) Context {
+ return internal.Builder{}.New(mode...)
+}
diff --git a/pkg/contexts/ocm/cpi/compose_test.go b/api/ocm/cpi/compose_test.go
similarity index 80%
rename from pkg/contexts/ocm/cpi/compose_test.go
rename to api/ocm/cpi/compose_test.go
index a33277a14..29c9161b9 100644
--- a/pkg/contexts/ocm/cpi/compose_test.go
+++ b/api/ocm/cpi/compose_test.go
@@ -5,22 +5,22 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/ocm/testhelper"
+ . "ocm.software/ocm/api/ocm/testhelper"
"github.com/mandelsoft/vfs/pkg/memoryfs"
"github.com/mandelsoft/vfs/pkg/vfs"
- "github.com/open-component-model/ocm/pkg/blobaccess"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/datacontext"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/compositionmodeattr"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/resourcetypes"
- "github.com/open-component-model/ocm/pkg/mime"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compositionmodeattr"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
)
const (
diff --git a/pkg/contexts/ocm/cpi/dummy.go b/api/ocm/cpi/dummy.go
similarity index 94%
rename from pkg/contexts/ocm/cpi/dummy.go
rename to api/ocm/cpi/dummy.go
index 113e9cef1..29d03a94c 100644
--- a/pkg/contexts/ocm/cpi/dummy.go
+++ b/api/ocm/cpi/dummy.go
@@ -5,12 +5,12 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/internal"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/refsel"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/rscsel"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/srcsel"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/ocm/selectors/refsel"
+ "ocm.software/ocm/api/ocm/selectors/rscsel"
+ "ocm.software/ocm/api/ocm/selectors/srcsel"
)
type DummyComponentVersionAccess struct {
diff --git a/api/ocm/cpi/interface.go b/api/ocm/cpi/interface.go
new file mode 100644
index 000000000..3665e3e00
--- /dev/null
+++ b/api/ocm/cpi/interface.go
@@ -0,0 +1,232 @@
+package cpi
+
+// This is the Context Provider Interface for credential providers
+
+import (
+ _ "unsafe"
+
+ "github.com/mandelsoft/goutils/sliceutils"
+ "github.com/mandelsoft/logging"
+
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/utils/registrations"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const CONTEXT_TYPE = internal.CONTEXT_TYPE
+
+const CommonTransportFormat = internal.CommonTransportFormat
+
+var TAG_BLOBHANDLER = logging.DefineTag("blobhandler", "execution of blob handler used to upload resource blobs to an ocm repository.")
+
+func BlobHandlerLogger(ctx Context, messageContext ...logging.MessageContext) logging.Logger {
+ if len(messageContext) > 0 {
+ messageContext = sliceutils.CopyAppend[logging.MessageContext](messageContext, TAG_BLOBHANDLER)
+ return ctx.Logger(messageContext...)
+ } else {
+ return ctx.Logger(TAG_BLOBHANDLER)
+ }
+}
+
+type (
+ Context = internal.Context
+ ContextProvider = internal.ContextProvider
+ LocalContextProvider = internal.LocalContextProvider
+ ComponentVersionResolver = internal.ComponentVersionResolver
+ Repository = internal.Repository
+ RepositoryTypeProvider = internal.RepositoryTypeProvider
+ RepositoryTypeScheme = internal.RepositoryTypeScheme
+ RepositoryDelegationRegistry = internal.RepositoryDelegationRegistry
+ RepositoryPriorityDecoder = internal.PriorityDecoder[Context, RepositorySpec]
+ RepositorySpecHandlers = internal.RepositorySpecHandlers
+ RepositorySpecHandler = internal.RepositorySpecHandler
+ UniformRepositorySpec = internal.UniformRepositorySpec
+ ComponentLister = internal.ComponentLister
+ ComponentAccess = internal.ComponentAccess
+ ComponentVersionAccess = internal.ComponentVersionAccess
+ AccessSpec = internal.AccessSpec
+ AccessSpecDecoder = internal.AccessSpecDecoder
+ GenericAccessSpec = internal.GenericAccessSpec
+ AccessMethod = internal.AccessMethod
+ AccessProvider = internal.AccessProvider
+ AccessTypeProvider = internal.AccessTypeProvider
+ AccessTypeScheme = internal.AccessTypeScheme
+ DataAccess = internal.DataAccess
+ BlobAccess = internal.BlobAccess
+ SourceAccess = internal.SourceAccess
+ SourceMeta = internal.SourceMeta
+ ResourceAccess = internal.ResourceAccess
+ ResourceMeta = internal.ResourceMeta
+ RepositorySpec = internal.RepositorySpec
+ RepositorySpecDecoder = internal.RepositorySpecDecoder
+ IntermediateRepositorySpecAspect = internal.IntermediateRepositorySpecAspect
+ GenericRepositorySpec = internal.GenericRepositorySpec
+ RepositoryType = internal.RepositoryType
+ ComponentReference = internal.ComponentReference
+)
+
+type ArtifactAccess[M any] internal.ArtifactAccess[M]
+
+type (
+ BlobHandler = internal.BlobHandler
+ BlobHandlerProvider = internal.BlobHandlerProvider
+ BlobHandlerOption = internal.BlobHandlerOption
+ BlobHandlerOptions = internal.BlobHandlerOptions
+ BlobHandlerKey = internal.BlobHandlerKey
+ BlobHandlerRegistry = internal.BlobHandlerRegistry
+ StorageContext = internal.StorageContext
+ ImplementationRepositoryType = internal.ImplementationRepositoryType
+
+ BlobHandlerConfig = internal.BlobHandlerConfig
+ BlobHandlerRegistrationHandler = internal.BlobHandlerRegistrationHandler
+)
+
+type (
+ DigesterType = internal.DigesterType
+ BlobDigester = internal.BlobDigester
+ BlobDigesterRegistry = internal.BlobDigesterRegistry
+ DigestDescriptor = internal.DigestDescriptor
+ HasherProvider = internal.HasherProvider
+ Hasher = internal.Hasher
+)
+
+type NamePath = registrations.NamePath
+
+func NewNamePath(p string) NamePath {
+ return registrations.NewNamePath(p)
+}
+
+func FromProvider(p ContextProvider) Context {
+ return internal.FromProvider(p)
+}
+
+func NewBlobHandlerOptions(olist ...BlobHandlerOption) *BlobHandlerOptions {
+ return internal.NewBlobHandlerOptions(olist...)
+}
+
+func DefaultBlobHandlerProvider(ctx Context) BlobHandlerProvider {
+ return internal.DefaultBlobHandlerProvider(ctx)
+}
+
+func NewResourceMeta(name string, typ string, relation metav1.ResourceRelation) *ResourceMeta {
+ return compdesc.NewResourceMeta(name, typ, relation)
+}
+
+func NewDigestDescriptor(digest string, typ DigesterType) *DigestDescriptor {
+ return internal.NewDigestDescriptor(digest, typ.HashAlgorithm, typ.NormalizationAlgorithm)
+}
+
+func DefaultBlobDigesterRegistry() BlobDigesterRegistry {
+ return internal.DefaultBlobDigesterRegistry
+}
+
+func DefaultDelegationRegistry() RepositoryDelegationRegistry {
+ return internal.DefaultRepositoryDelegationRegistry
+}
+
+func DefaultContext() internal.Context {
+ return internal.DefaultContext
+}
+
+func WithPrio(p int) BlobHandlerOption {
+ return internal.WithPrio(p)
+}
+
+func ForRepo(ctxtype, repostype string) BlobHandlerOption {
+ return internal.ForRepo(ctxtype, repostype)
+}
+
+func ForMimeType(mimetype string) BlobHandlerOption {
+ return internal.ForMimeType(mimetype)
+}
+
+func ForArtifactType(arttype string) BlobHandlerOption {
+ return internal.ForArtifactType(arttype)
+}
+
+func RegisterRepositorySpecHandler(handler RepositorySpecHandler, types ...string) {
+ internal.RegisterRepositorySpecHandler(handler, types...)
+}
+
+func RegisterBlobHandler(handler BlobHandler, opts ...BlobHandlerOption) {
+ internal.RegisterBlobHandler(handler, opts...)
+}
+
+func RegisterBlobHandlerRegistrationHandler(path string, handler BlobHandlerRegistrationHandler) {
+ internal.RegisterBlobHandlerRegistrationHandler(path, handler)
+}
+
+func MustRegisterDigester(digester BlobDigester, arttypes ...string) {
+ internal.MustRegisterDigester(digester, arttypes...)
+}
+
+func SetDefaultDigester(d BlobDigester) {
+ internal.SetDefaultDigester(d)
+}
+
+func ToGenericAccessSpec(spec AccessSpec) (*GenericAccessSpec, error) {
+ return internal.ToGenericAccessSpec(spec)
+}
+
+func ToGenericRepositorySpec(spec RepositorySpec) (*GenericRepositorySpec, error) {
+ return internal.ToGenericRepositorySpec(spec)
+}
+
+type AccessSpecRef = internal.AccessSpecRef
+
+func NewAccessSpecRef(spec AccessSpec) *AccessSpecRef {
+ return internal.NewAccessSpecRef(spec)
+}
+
+func NewRawAccessSpecRef(data []byte, unmarshaler runtime.Unmarshaler) (*AccessSpecRef, error) {
+ return internal.NewRawAccessSpecRef(data, unmarshaler)
+}
+
+const (
+ KIND_REPOSITORY = internal.KIND_REPOSITORY
+ KIND_COMPONENTVERSION = internal.KIND_COMPONENTVERSION
+ KIND_RESOURCE = internal.KIND_RESOURCE
+ KIND_SOURCE = internal.KIND_SOURCE
+ KIND_REFERENCE = internal.KIND_REFERENCE
+)
+
+func ErrComponentVersionNotFound(name, version string) error {
+ return internal.ErrComponentVersionNotFound(name, version)
+}
+
+func ErrComponentVersionNotFoundWrap(err error, name, version string) error {
+ return internal.ErrComponentVersionNotFoundWrap(err, name, version)
+}
+
+// PrefixProvider is supported by RepositorySpecs to
+// provide info about a potential path prefix to
+// use for globalized local artifacts.
+type PrefixProvider interface {
+ PathPrefix() string
+}
+
+func RepositoryPrefix(spec RepositorySpec) string {
+ if s, ok := spec.(PrefixProvider); ok {
+ return s.PathPrefix()
+ }
+ return ""
+}
+
+// HintProvider is able to provide a name hint for globalization of local
+// artifacts.
+type HintProvider internal.HintProvider
+
+// GlobalAccessProvider is able to provide a non-local access specification.
+type GlobalAccessProvider internal.GlobalAccessProvider
+
+// provide context interface for other files to avoid diffs in imports.
+var (
+ newStrictRepositoryTypeScheme = internal.NewStrictRepositoryTypeScheme
+ defaultRepositoryTypeScheme = internal.DefaultRepositoryTypeScheme
+)
+
+func WrapContextProvider(ctx LocalContextProvider) ContextProvider {
+ return internal.WrapContextProvider(ctx)
+}
diff --git a/pkg/contexts/ocm/cpi/logging.go b/api/ocm/cpi/logging.go
similarity index 100%
rename from pkg/contexts/ocm/cpi/logging.go
rename to api/ocm/cpi/logging.go
diff --git a/api/ocm/cpi/modopts.go b/api/ocm/cpi/modopts.go
new file mode 100644
index 000000000..10ef7f7bb
--- /dev/null
+++ b/api/ocm/cpi/modopts.go
@@ -0,0 +1,114 @@
+package cpi
+
+import (
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/attrs/hashattr"
+ "ocm.software/ocm/api/ocm/extensions/attrs/signingattr"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/tech/signing/hasher/sha256"
+)
+
+type (
+ TargetElement = internal.TargetElement
+ TargetOption = internal.TargetOption
+ TargetOptions = internal.TargetOptions
+
+ ModificationOption = internal.ModificationOption
+ ModificationOptions = internal.ModificationOptions
+
+ BlobModificationOption = internal.BlobModificationOption
+ BlobModificationOptions = internal.BlobModificationOptions
+
+ BlobUploadOption = internal.BlobUploadOption
+ BlobUploadOptions = internal.BlobUploadOptions
+
+ AddVersionOption = internal.AddVersionOption
+ AddVersionOptions = internal.AddVersionOptions
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewAddVersionOptions(list ...AddVersionOption) *AddVersionOptions {
+ return internal.NewAddVersionOptions(list...)
+}
+
+// Overwrite enabled the overwrite mode for adding a component version.
+func Overwrite(flag ...bool) AddVersionOption {
+ return internal.Overwrite(flag...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewBlobModificationOptions(list ...BlobModificationOption) *BlobModificationOptions {
+ return internal.NewBlobModificationOptions(list...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewBlobUploadOptions(list ...BlobUploadOption) *BlobUploadOptions {
+ return internal.NewBlobUploadOptions(list...)
+}
+
+func UseBlobHandlers(h BlobHandlerProvider) internal.BlobOptionImpl {
+ return internal.UseBlobHandlers(h)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewModificationOptions(list ...ModificationOption) *ModificationOptions {
+ return internal.NewModificationOptions(list...)
+}
+
+func TargetIndex(idx int) internal.TargetIndex {
+ return internal.TargetIndex(-1)
+}
+
+const AppendElement = internal.TargetIndex(-1)
+
+var UpdateElement = internal.UpdateElement
+
+func TargetIdentity(id v1.Identity) internal.TargetIdentity {
+ return internal.TargetIdentity(id)
+}
+
+func ModifyResource(flag ...bool) internal.ModOptionImpl {
+ return internal.ModifyResource(flag...)
+}
+
+func AcceptExistentDigests(flag ...bool) internal.ModOptionImpl {
+ return internal.AcceptExistentDigests(flag...)
+}
+
+func WithDefaultHashAlgorithm(algo ...string) internal.ModOptionImpl {
+ return internal.WithDefaultHashAlgorithm(algo...)
+}
+
+func WithHasherProvider(prov HasherProvider) internal.ModOptionImpl {
+ return internal.WithHasherProvider(prov)
+}
+
+func SkipVerify(flag ...bool) internal.ModOptionImpl {
+ return internal.SkipVerify(flag...)
+}
+
+// SkipDigest disables digest creation if enabled.
+//
+// Deprecated: for legacy code, only.
+func SkipDigest(flag ...bool) internal.ModOptionImpl {
+ return internal.SkipDigest(flag...)
+}
+
+///////////////////////////////////////////////////////
+
+func CompleteModificationOptions(ctx ContextProvider, m *ModificationOptions) {
+ attr := hashattr.Get(ctx.OCMContext())
+ if m.DefaultHashAlgorithm == "" {
+ m.DefaultHashAlgorithm = attr.DefaultHasher
+ }
+ if m.DefaultHashAlgorithm == "" {
+ m.DefaultHashAlgorithm = sha256.Algorithm
+ }
+ if m.HasherProvider == nil {
+ m.HasherProvider = signingattr.Get(ctx.OCMContext())
+ }
+}
diff --git a/pkg/contexts/ocm/cpi/ref.go b/api/ocm/cpi/ref.go
similarity index 100%
rename from pkg/contexts/ocm/cpi/ref.go
rename to api/ocm/cpi/ref.go
diff --git a/api/ocm/cpi/repocpi/README.md b/api/ocm/cpi/repocpi/README.md
new file mode 100644
index 000000000..05b9206f4
--- /dev/null
+++ b/api/ocm/cpi/repocpi/README.md
@@ -0,0 +1,77 @@
+# Context Programming Interface for Repositories
+
+Package repocpi contains the implementation support
+ for repository backends. It offers three methods
+ to create component version, component and repository
+ objects based on three simple implementation interfaces.
+
+ The basic provisioning model is layered:
+
+ ![Implamentation Layers](ocmimpllayers.png)
+
+ - on layer 1 there is the *user facing API* defined
+ in package [ocm.software/ocm/api/ocm].
+
+ - on layer 2 (this package) there is a backend agnostic
+ implementation of standard functionality based on layer 3.
+ This is divided into two parts
+
+ a) the *view* objects provided by the `Dup()` calls of the layer 1 API.
+ All dups are internally based on a single base object.
+ These objects are called *bridge*. They act as base object
+ for the views and as abstraction for the implementation objects
+ providing *generic* implementations potentially based on
+ the implementation functionality.
+ (see bridge design pattern https://refactoring.guru/design-patterns/bridge)
+
+ b) the *bridge* object as base for all dup views is used to implement some
+ common functionality like the view management. The bridge object
+ is closed, when the last view disappears.
+ This bridge object then calls the final
+ storage backend implementation interface.
+
+ - the storage backend implementations based on the implementation
+ interfaces provided by layer 2.
+
+ The implementation interfaces and the functions to create API objects are:
+
+ - interface [ComponentVersionAccessImpl] is used to create an ocm.ComponentVersionAccess object
+ using the function [NewComponentVersionAccess].
+ - interface [ComponentAccessImpl] is used to create an ocm.ComponentAccess object
+ using the function [NewComponentAccess].
+ - interface [RepositoryImpl] is used to create an ocm.ComponentAccess object
+ using the function [NewRepository].
+
+ Component version implementations provide basic access to component versions
+ and their descriptors. They keep a reference to component implementations, which are
+ again based on repository implementations. The task of repository implementations is
+ to provide component objects. Their implementations are responsible to provide
+ component version objects.
+
+## Simplified Respository Implementation Interface
+ Besides this basic implementation interfaces with separated objects for a
+ repository, component and component version, there is support for a simplified
+ implementation interface (`StorageBackendImpl`). This is a single interface
+ bundling all required functionality to implement the objects for the three
+ concerned elements. With `NewStorageBackend` it is possible to instantiate
+ a new kind of repository based on this single interface. The required
+ objects for components and component versions are generically provided
+ based on the methods provided by this interface.
+
+## Comparison of Implementation Models
+
+The simplified implementation model does not provide access to the
+implementation objects for components and component versions.
+Therefore, it is not possible to keep state for those elements.
+
+Storage Backend Implementations requiring such state, like the OCI
+implementation based on the OCI abstraction provided by the OCI
+context, therefore use dedicated implementations for repository,
+component and component version objects. This model provides
+complete control over the lifecycle of those elements.
+
+If a storage backend implementation is stateless or just keeps
+state at the repository level, the simplified implementation model
+can be chosen.
+
+
diff --git a/pkg/contexts/ocm/cpi/repocpi/backend.go b/api/ocm/cpi/repocpi/backend.go
similarity index 96%
rename from pkg/contexts/ocm/cpi/repocpi/backend.go
rename to api/ocm/cpi/repocpi/backend.go
index efa1736ca..166c420e5 100644
--- a/pkg/contexts/ocm/cpi/repocpi/backend.go
+++ b/api/ocm/cpi/repocpi/backend.go
@@ -6,11 +6,11 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/refmgmt"
)
// StorageBackendImpl is an interface which can be implemented
diff --git a/pkg/contexts/ocm/cpi/repocpi/blobcache.go b/api/ocm/cpi/repocpi/blobcache.go
similarity index 96%
rename from pkg/contexts/ocm/cpi/repocpi/blobcache.go
rename to api/ocm/cpi/repocpi/blobcache.go
index 83edc2876..73cd08a18 100644
--- a/pkg/contexts/ocm/cpi/repocpi/blobcache.go
+++ b/api/ocm/cpi/repocpi/blobcache.go
@@ -5,7 +5,7 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
)
type (
diff --git a/pkg/contexts/ocm/cpi/repocpi/bridge_c.go b/api/ocm/cpi/repocpi/bridge_c.go
similarity index 93%
rename from pkg/contexts/ocm/cpi/repocpi/bridge_c.go
rename to api/ocm/cpi/repocpi/bridge_c.go
index eeac749dc..e4ffaa7d7 100644
--- a/pkg/contexts/ocm/cpi/repocpi/bridge_c.go
+++ b/api/ocm/cpi/repocpi/bridge_c.go
@@ -7,12 +7,12 @@ import (
"github.com/mandelsoft/goutils/finalizer"
"github.com/mandelsoft/goutils/optionutils"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/compositionmodeattr"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compositionmodeattr"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/refmgmt"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
)
type ComponentVersionAccessInfo struct {
diff --git a/pkg/contexts/ocm/cpi/repocpi/bridge_cv.go b/api/ocm/cpi/repocpi/bridge_cv.go
similarity index 93%
rename from pkg/contexts/ocm/cpi/repocpi/bridge_cv.go
rename to api/ocm/cpi/repocpi/bridge_cv.go
index d2b39120b..b7529468c 100644
--- a/pkg/contexts/ocm/cpi/repocpi/bridge_cv.go
+++ b/api/ocm/cpi/repocpi/bridge_cv.go
@@ -10,21 +10,21 @@ import (
"github.com/mandelsoft/goutils/finalizer"
"github.com/mandelsoft/goutils/optionutils"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/compose"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/compositionmodeattr"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/keepblobattr"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/internal"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/pubsub"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
- "github.com/open-component-model/ocm/pkg/runtimefinalizer"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/compose"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compositionmodeattr"
+ "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/refmgmt"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
)
// here, we define the common implementation agnostic parts
diff --git a/pkg/contexts/ocm/cpi/repocpi/bridge_r.go b/api/ocm/cpi/repocpi/bridge_r.go
similarity index 92%
rename from pkg/contexts/ocm/cpi/repocpi/bridge_r.go
rename to api/ocm/cpi/repocpi/bridge_r.go
index 127f14da7..f7eea176f 100644
--- a/pkg/contexts/ocm/cpi/repocpi/bridge_r.go
+++ b/api/ocm/cpi/repocpi/bridge_r.go
@@ -5,10 +5,10 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/refmgmt"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
)
type ComponentAccessInfo struct {
diff --git a/api/ocm/cpi/repocpi/doc.go b/api/ocm/cpi/repocpi/doc.go
new file mode 100644
index 000000000..df1f45f0a
--- /dev/null
+++ b/api/ocm/cpi/repocpi/doc.go
@@ -0,0 +1,55 @@
+// Package repocpi contains the implementation support
+// for repository backends. It offers three methods
+// to create component version, component and repository
+// objects based on three simple implementation interfaces.
+//
+// The basic provisioning model is layered:
+//
+// - on layer 1 there is the user facing API defined
+// in package [ocm.software/ocm/api/ocm].
+//
+// - on layer 2 (this package) there is a backend agnostic
+// implementation of standard functionality based on layer 3.
+// This is divided into two parts
+//
+// a) the view objects provided by the Dup() calls of the layer 1 API.
+// All dups are internally based on a single base object.
+// These objects are called bridge. They act as base object
+// for the views and as abstraction for the implementation objects
+// providing generic implementations potentially based on
+// the implementation functionality.
+// (see bridge design pattern https://refactoring.guru/design-patterns/bridge)
+//
+// b) the bridge object as base for all dup views is used to implement some
+// common functionality like the view management. The bridge object
+// is closed, when the last view disappears.
+// This bridge object then calls the final
+// storage backend implementation interface.
+//
+// - the storage backend implementations based on the implementation
+// interfaces provided by layer 2.
+//
+// The implementation interfaces and the functions to create API objects are:
+//
+// - interface [ComponentVersionAccessImpl] is used to create an ocm.ComponentVersionAccess object
+// using the function [NewComponentVersionAccess].
+// - interface [ComponentAccessImpl] is used to create an ocm.ComponentAccess object
+// using the function [NewComponentAccess].
+// - interface [RepositoryImpl] is used to create an ocm.ComponentAccess object
+// using the function [NewRepository].
+//
+// Component version implementations provide basic access to component versions
+// and their descriptors. They keep a reference to component implementations, which are
+// again based on repository implementations. The task of repository implementations is
+// to provide component objects. Their implementations are responsible to provide
+// component version objects.
+//
+// Besides this basic implementation interface with separated object for a
+// repository, component and component version, there is support for a simplified
+// implementation interface (StorageBackendImpl). This is a single interface
+// bundling all required functionality to implement the objects for the three
+// concerned elements. With NewStorageBackend it is possible to instantiate
+// a new kind of repository based on this single interface. The required
+// objects for components and component versions are generically provided
+// based on the methods provided by this interface.
+package repocpi
diff --git a/pkg/contexts/ocm/cpi/repocpi/helperinterfaces.go b/api/ocm/cpi/repocpi/helperinterfaces.go
similarity index 89%
rename from pkg/contexts/ocm/cpi/repocpi/helperinterfaces.go
rename to api/ocm/cpi/repocpi/helperinterfaces.go
index 4322bf2bd..5a9e2879f 100644
--- a/pkg/contexts/ocm/cpi/repocpi/helperinterfaces.go
+++ b/api/ocm/cpi/repocpi/helperinterfaces.go
@@ -3,8 +3,8 @@ package repocpi
import (
"fmt"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
)
var (
diff --git a/api/ocm/cpi/repocpi/interface.go b/api/ocm/cpi/repocpi/interface.go
new file mode 100644
index 000000000..057902c09
--- /dev/null
+++ b/api/ocm/cpi/repocpi/interface.go
@@ -0,0 +1,7 @@
+package repocpi
+
+import (
+ "ocm.software/ocm/api/ocm/internal"
+)
+
+type Repository = internal.Repository
diff --git a/pkg/contexts/ocm/cpi/repocpi/ocmimpllayers.png b/api/ocm/cpi/repocpi/ocmimpllayers.png
similarity index 100%
rename from pkg/contexts/ocm/cpi/repocpi/ocmimpllayers.png
rename to api/ocm/cpi/repocpi/ocmimpllayers.png
diff --git a/pkg/contexts/ocm/cpi/repocpi/view_c.go b/api/ocm/cpi/repocpi/view_c.go
similarity index 94%
rename from pkg/contexts/ocm/cpi/repocpi/view_c.go
rename to api/ocm/cpi/repocpi/view_c.go
index a35db654f..c4f5727c7 100644
--- a/pkg/contexts/ocm/cpi/repocpi/view_c.go
+++ b/api/ocm/cpi/repocpi/view_c.go
@@ -6,10 +6,10 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
)
type _componentAccessView interface {
diff --git a/pkg/contexts/ocm/cpi/repocpi/view_cv.go b/api/ocm/cpi/repocpi/view_cv.go
similarity index 96%
rename from pkg/contexts/ocm/cpi/repocpi/view_cv.go
rename to api/ocm/cpi/repocpi/view_cv.go
index ea2a24d97..1e5e1c59e 100644
--- a/pkg/contexts/ocm/cpi/repocpi/view_cv.go
+++ b/api/ocm/cpi/repocpi/view_cv.go
@@ -7,23 +7,23 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/internal"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/descriptor"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/refsel"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/rscsel"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/selectors/srcsel"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
- "github.com/open-component-model/ocm/pkg/utils"
- "github.com/open-component-model/ocm/pkg/utils/selector"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/selectors"
+ "ocm.software/ocm/api/ocm/selectors/refsel"
+ "ocm.software/ocm/api/ocm/selectors/rscsel"
+ "ocm.software/ocm/api/ocm/selectors/srcsel"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/refmgmt"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
+ "ocm.software/ocm/api/utils/selector"
)
// View objects are the user facing generic implementations of the context interfaces.
diff --git a/pkg/contexts/ocm/cpi/repocpi/view_r.go b/api/ocm/cpi/repocpi/view_r.go
similarity index 94%
rename from pkg/contexts/ocm/cpi/repocpi/view_r.go
rename to api/ocm/cpi/repocpi/view_r.go
index 65d067f6b..4fc9d5a42 100644
--- a/pkg/contexts/ocm/cpi/repocpi/view_r.go
+++ b/api/ocm/cpi/repocpi/view_r.go
@@ -6,11 +6,11 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/contexts/credentials"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/refmgmt"
- "github.com/open-component-model/ocm/pkg/refmgmt/resource"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/refmgmt"
+ "ocm.software/ocm/api/utils/refmgmt/resource"
)
// View objects are the user facing generic implementations of the context interfaces.
diff --git a/api/ocm/cpi/repotypes.go b/api/ocm/cpi/repotypes.go
new file mode 100644
index 000000000..95d5f15d7
--- /dev/null
+++ b/api/ocm/cpi/repotypes.go
@@ -0,0 +1,59 @@
+package cpi
+
+// this file is similar to contexts oci.
+
+import (
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type RepositoryTypeVersionScheme = runtime.TypeVersionScheme[RepositorySpec, RepositoryType]
+
+func NewRepositoryTypeVersionScheme(kind string) RepositoryTypeVersionScheme {
+ return runtime.NewTypeVersionScheme[RepositorySpec, RepositoryType](kind, newStrictRepositoryTypeScheme())
+}
+
+func RegisterRepositoryType(rtype RepositoryType) {
+ defaultRepositoryTypeScheme.Register(rtype)
+}
+
+func RegisterRepositoryTypeVersions(s RepositoryTypeVersionScheme) {
+ defaultRepositoryTypeScheme.AddKnownTypes(s)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type repositoryType struct {
+ runtime.VersionedTypedObjectType[RepositorySpec]
+ checker RepositoryAccessMethodChecker
+}
+
+type RepositoryAccessMethodChecker func(Context, compdesc.AccessSpec) bool
+
+func NewRepositoryType[I RepositorySpec](name string, checker RepositoryAccessMethodChecker) RepositoryType {
+ return &repositoryType{
+ VersionedTypedObjectType: runtime.NewVersionedTypedObjectType[RepositorySpec, I](name),
+ checker: checker,
+ }
+}
+
+func NewRepositoryTypeByConverter[I RepositorySpec, V runtime.VersionedTypedObject](name string, converter runtime.Converter[I, V], checker RepositoryAccessMethodChecker) RepositoryType {
+ return &repositoryType{
+ VersionedTypedObjectType: runtime.NewVersionedTypedObjectTypeByConverter[RepositorySpec, I, V](name, converter),
+ checker: checker,
+ }
+}
+
+func NewRepositoryTypeByFormatVersion(name string, fmt runtime.FormatVersion[RepositorySpec], checker RepositoryAccessMethodChecker) RepositoryType {
+ return &repositoryType{
+ VersionedTypedObjectType: runtime.NewVersionedTypedObjectTypeByFormatVersion[RepositorySpec](name, fmt),
+ checker: checker,
+ }
+}
+
+func (t *repositoryType) LocalSupportForAccessSpec(ctx Context, a compdesc.AccessSpec) bool {
+ if t.checker != nil {
+ return t.checker(ctx, a)
+ }
+ return false
+}
diff --git a/pkg/contexts/ocm/cpi/storagectx.go b/api/ocm/cpi/storagectx.go
similarity index 100%
rename from pkg/contexts/ocm/cpi/storagectx.go
rename to api/ocm/cpi/storagectx.go
diff --git a/pkg/contexts/ocm/cpi/suite_test.go b/api/ocm/cpi/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/cpi/suite_test.go
rename to api/ocm/cpi/suite_test.go
diff --git a/api/ocm/cpi/utils.go b/api/ocm/cpi/utils.go
new file mode 100644
index 000000000..433cb3cad
--- /dev/null
+++ b/api/ocm/cpi/utils.go
@@ -0,0 +1,89 @@
+package cpi
+
+import (
+ "io"
+
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/iotools"
+)
+
+type AccessMethodSource interface {
+ AccessMethod() (AccessMethod, error)
+}
+
+// ResourceReader gets a Reader for a given resource/source access.
+// It provides a Reader handling the Close contract for the access method
+// by connecting the access method's Close method to the Readers Close method .
+// Deprecated: use GetResourceReader.
+// It must be deprecated because of the support of free-floating ReSourceAccess
+// implementations, they not necessarily provide an AccessMethod.
+func ResourceReader(s AccessMethodSource) (io.ReadCloser, error) {
+ meth, err := s.AccessMethod()
+ if err != nil {
+ return nil, err
+ }
+ return toResourceReaderForMethod(meth)
+}
+
+// ResourceMimeReader gets a Reader for a given resource/source access.
+// It provides a Reader handling the Close contract for the access method
+// by connecting the access method's Close method to the Readers Close method.
+// Additionally, the mime type is returned.
+// Deprecated: use GetResourceMimeReader.
+// It must be deprecated because of the support of free-floating ReSourceAccess
+// implementations, they not necessarily provide an AccessMethod.
+func ResourceMimeReader(s AccessMethodSource) (io.ReadCloser, string, error) {
+ meth, err := s.AccessMethod()
+ if err != nil {
+ return nil, "", err
+ }
+ r, err := toResourceReaderForMethod(meth)
+ return r, meth.MimeType(), err
+}
+
+func toResourceReaderForMethod(meth AccessMethod) (io.ReadCloser, error) {
+ r, err := meth.Reader()
+ if err != nil {
+ meth.Close()
+ return nil, err
+ }
+ return iotools.AddReaderCloser(r, meth, "access method"), nil
+}
+
+// GetResourceMimeReader gets a Reader for a given resource/source access.
+// It provides a Reader handling the Close contract for the access method.
+func GetResourceReader(acc AccessProvider) (io.ReadCloser, error) {
+ return blobaccess.ReaderFromProvider(acc)
+}
+
+// GetResourceMimeReader gets a Reader for a given resource/source access.
+// It provides a Reader handling the Close contract for the access method.
+// Additionally, the mime type is returned.
+func GetResourceMimeReader(acc AccessProvider) (io.ReadCloser, string, error) {
+ return blobaccess.MimeReaderFromProvider(acc)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func ArtifactNameHint(spec AccessSpec, cv ComponentVersionAccess) string {
+ if h, ok := spec.(HintProvider); ok {
+ return h.GetReferenceHint(cv)
+ }
+ return ""
+}
+
+func ReferenceHint(spec AccessSpec, cv ComponentVersionAccess) string {
+ if h, ok := spec.(internal.HintProvider); ok {
+ return h.GetReferenceHint(cv)
+ }
+ return ""
+}
+
+func GlobalAccess(spec AccessSpec, ctx Context) AccessSpec {
+ g := spec.GlobalAccessSpec(ctx)
+ if g != nil && g.IsLocal(ctx) {
+ g = nil
+ }
+ return g
+}
diff --git a/pkg/contexts/ocm/cpi/view_rsc.go b/api/ocm/cpi/view_rsc.go
similarity index 93%
rename from pkg/contexts/ocm/cpi/view_rsc.go
rename to api/ocm/cpi/view_rsc.go
index 6baca3c0a..55dc9c5e5 100644
--- a/pkg/contexts/ocm/cpi/view_rsc.go
+++ b/api/ocm/cpi/view_rsc.go
@@ -5,13 +5,13 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess"
- "github.com/open-component-model/ocm/pkg/contexts/credentials"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- ocm "github.com/open-component-model/ocm/pkg/contexts/ocm/context"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- cpi "github.com/open-component-model/ocm/pkg/contexts/ocm/internal"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ cpi "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ ocm "ocm.software/ocm/api/ocm/types"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
)
////////////////////////////////////////////////////////////////////////////////
diff --git a/pkg/contexts/ocm/elements/artifactaccess/doc.go b/api/ocm/elements/artifactaccess/doc.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactaccess/doc.go
rename to api/ocm/elements/artifactaccess/doc.go
diff --git a/api/ocm/elements/artifactaccess/genericaccess/resource.go b/api/ocm/elements/artifactaccess/genericaccess/resource.go
new file mode 100644
index 000000000..42439b901
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/genericaccess/resource.go
@@ -0,0 +1,34 @@
+package genericaccess
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/generics"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, access ocm.AccessSpec) (cpi.ArtifactAccess[M], error) {
+ prov, err := cpi.NewAccessProviderForExternalAccessSpec(ctx, access)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid external access method %q", access.GetKind())
+ }
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), prov), nil
+}
+
+func MustAccess[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, access ocm.AccessSpec) cpi.ArtifactAccess[M] {
+ a, err := Access(ctx, meta, access)
+ if err != nil {
+ panic(err)
+ }
+ return a
+}
+
+func ResourceAccess(ctx ocm.Context, meta *ocm.ResourceMeta, access ocm.AccessSpec) (cpi.ResourceAccess, error) {
+ return Access(ctx, meta, access)
+}
+
+func SourceAccess(ctx ocm.Context, meta *ocm.SourceMeta, access ocm.AccessSpec) (cpi.SourceAccess, error) {
+ return Access(ctx, meta, access)
+}
diff --git a/api/ocm/elements/artifactaccess/genericaccess/resource_test.go b/api/ocm/elements/artifactaccess/genericaccess/resource_test.go
new file mode 100644
index 000000000..599ff690c
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/genericaccess/resource_test.go
@@ -0,0 +1,55 @@
+package genericaccess_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm/compdesc"
+ me "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/accessio"
+)
+
+const (
+ OCIPATH = "/tmp/oci"
+ OCIHOST = "alias"
+)
+
+var _ = Describe("dir tree resource access", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ OCIManifest1(env)
+ })
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("creates resource", func() {
+ spec := ociartifact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION))
+
+ acc := Must(me.ResourceAccess(env.OCMContext(), compdesc.NewResourceMeta("test", resourcetypes.OCI_IMAGE, compdesc.LocalRelation), spec))
+
+ Expect(acc.ReferenceHint()).To(Equal(OCINAMESPACE + ":" + OCIVERSION))
+ Expect(acc.GlobalAccess()).To(BeNil())
+ Expect(acc.Meta().Type).To(Equal(resourcetypes.OCI_IMAGE))
+
+ blob := Must(acc.BlobAccess())
+ defer Defer(blob.Close, "blob")
+ Expect(blob.MimeType()).To(Equal(artifactset.MediaType(artdesc.MediaTypeImageManifest)))
+ })
+})
diff --git a/pkg/contexts/ocm/elements/artifactaccess/genericaccess/suite_test.go b/api/ocm/elements/artifactaccess/genericaccess/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactaccess/genericaccess/suite_test.go
rename to api/ocm/elements/artifactaccess/genericaccess/suite_test.go
diff --git a/pkg/contexts/ocm/elements/artifactaccess/githubaccess/options.go b/api/ocm/elements/artifactaccess/githubaccess/options.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactaccess/githubaccess/options.go
rename to api/ocm/elements/artifactaccess/githubaccess/options.go
diff --git a/api/ocm/elements/artifactaccess/githubaccess/resource.go b/api/ocm/elements/artifactaccess/githubaccess/resource.go
new file mode 100644
index 000000000..fc668d38e
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/githubaccess/resource.go
@@ -0,0 +1,33 @@
+package githubaccess
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/github"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+)
+
+const TYPE = resourcetypes.DIRECTORY_TREE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, repo string, commit string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(repo, eff.APIHostName, commit)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, repo string, commit string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, repo, commit, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, repo string, commit string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, repo, commit, opts...)
+}
diff --git a/api/ocm/elements/artifactaccess/helmaccess/resource.go b/api/ocm/elements/artifactaccess/helmaccess/resource.go
new file mode 100644
index 000000000..866d11f80
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/helmaccess/resource.go
@@ -0,0 +1,30 @@
+package helmaccess
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/helm"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+)
+
+const TYPE = resourcetypes.HELM_CHART
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, chart string, repourl string) cpi.ArtifactAccess[M] {
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(chart, repourl)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, chart string, repourl string) cpi.ResourceAccess {
+ return Access(ctx, meta, chart, repourl)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, chart string, repourl string) cpi.SourceAccess {
+ return Access(ctx, meta, chart, repourl)
+}
diff --git a/api/ocm/elements/artifactaccess/mavenaccess/options.go b/api/ocm/elements/artifactaccess/mavenaccess/options.go
new file mode 100644
index 000000000..7c9ef12f7
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/mavenaccess/options.go
@@ -0,0 +1,20 @@
+package mavenaccess
+
+import "ocm.software/ocm/api/tech/maven"
+
+type (
+ Options = maven.Coordinates
+ Option = maven.CoordinateOption
+)
+
+type WithClassifier = maven.WithClassifier
+
+func WithOptionalClassifier(c *string) Option {
+ return maven.WithOptionalClassifier(c)
+}
+
+type WithExtension = maven.WithExtension
+
+func WithOptionalExtension(e *string) Option {
+ return maven.WithOptionalExtension(e)
+}
diff --git a/api/ocm/elements/artifactaccess/mavenaccess/resource.go b/api/ocm/elements/artifactaccess/mavenaccess/resource.go
new file mode 100644
index 000000000..72debf895
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/mavenaccess/resource.go
@@ -0,0 +1,39 @@
+package mavenaccess
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/maven"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/tech/maven"
+)
+
+const TYPE = resourcetypes.MAVEN_PACKAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, repoUrl, groupId, artifactId, version string, opts ...Option) cpi.ArtifactAccess[M] {
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(repoUrl, groupId, artifactId, version, opts...)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, repoUrl, groupId, artifactId, version string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, repoUrl, groupId, artifactId, version, opts...)
+}
+
+func ResourceAccessForMavenCoords(ctx ocm.Context, meta *cpi.ResourceMeta, repoUrl string, coords *maven.Coordinates) cpi.ResourceAccess {
+ return Access(ctx, meta, repoUrl, coords.GroupId, coords.ArtifactId, coords.Version, WithOptionalClassifier(coords.Classifier), WithOptionalExtension(coords.Extension))
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, repoUrl, groupId, artifactId, version string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, repoUrl, groupId, artifactId, version, opts...)
+}
+
+func SourceAccessForMavenCoords(ctx ocm.Context, meta *cpi.SourceMeta, repoUrl string, coords *maven.Coordinates) cpi.SourceAccess {
+ return Access(ctx, meta, repoUrl, coords.GroupId, coords.ArtifactId, coords.Version, WithOptionalClassifier(coords.Classifier), WithOptionalExtension(coords.Extension))
+}
diff --git a/api/ocm/elements/artifactaccess/npmaccess/resource.go b/api/ocm/elements/artifactaccess/npmaccess/resource.go
new file mode 100644
index 000000000..1bb5a0ba9
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/npmaccess/resource.go
@@ -0,0 +1,30 @@
+package npmaccess
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/npm"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+)
+
+const TYPE = resourcetypes.NPM_PACKAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, registry, pkg, version string) cpi.ArtifactAccess[M] {
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(registry, pkg, version)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, registry, pkg, version string) cpi.ResourceAccess {
+ return Access(ctx, meta, registry, pkg, version)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, registry, pkg, version string) cpi.SourceAccess {
+ return Access(ctx, meta, registry, pkg, version)
+}
diff --git a/api/ocm/elements/artifactaccess/ociartifactaccess/resource.go b/api/ocm/elements/artifactaccess/ociartifactaccess/resource.go
new file mode 100644
index 000000000..0c43847ee
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/ociartifactaccess/resource.go
@@ -0,0 +1,30 @@
+package ociartifactaccess
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+)
+
+const TYPE = resourcetypes.OCI_IMAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, refname string) cpi.ArtifactAccess[M] {
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(refname)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, path string) cpi.ResourceAccess {
+ return Access(ctx, meta, path)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, path string) cpi.SourceAccess {
+ return Access(ctx, meta, path)
+}
diff --git a/pkg/contexts/ocm/elements/artifactaccess/ociblobaccess/options.go b/api/ocm/elements/artifactaccess/ociblobaccess/options.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactaccess/ociblobaccess/options.go
rename to api/ocm/elements/artifactaccess/ociblobaccess/options.go
diff --git a/api/ocm/elements/artifactaccess/ociblobaccess/resource.go b/api/ocm/elements/artifactaccess/ociblobaccess/resource.go
new file mode 100644
index 000000000..fc46049ec
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/ociblobaccess/resource.go
@@ -0,0 +1,39 @@
+package github
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/ociblob"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const TYPE = resourcetypes.BLOB
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, repository string, digest digest.Digest, size int64, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ media := eff.MediaType
+ if media == "" {
+ media = mime.MIME_OCTET
+ }
+ spec := access.New(repository, digest, media, size)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, repository string, digest digest.Digest, size int64, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, repository, digest, size, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, repository string, digest digest.Digest, size int64, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, repository, digest, size, opts...)
+}
diff --git a/pkg/contexts/ocm/elements/artifactaccess/s3access/options.go b/api/ocm/elements/artifactaccess/s3access/options.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactaccess/s3access/options.go
rename to api/ocm/elements/artifactaccess/s3access/options.go
diff --git a/api/ocm/elements/artifactaccess/s3access/resource.go b/api/ocm/elements/artifactaccess/s3access/resource.go
new file mode 100644
index 000000000..f44a985f4
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/s3access/resource.go
@@ -0,0 +1,38 @@
+package github
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/s3"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const TYPE = resourcetypes.BLOB
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, bucket, key string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ media := eff.MediaType
+ if media == "" {
+ media = mime.MIME_OCTET
+ }
+ spec := access.New(eff.Region, bucket, key, eff.Version, media)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, bucket, key string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, bucket, key, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, bucket, key string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, bucket, key, opts...)
+}
diff --git a/api/ocm/elements/artifactaccess/wgetaccess/options.go b/api/ocm/elements/artifactaccess/wgetaccess/options.go
new file mode 100644
index 000000000..66f1ef0a4
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/wgetaccess/options.go
@@ -0,0 +1,48 @@
+package wgetaccess
+
+import (
+ "io"
+ "net/http"
+
+ "github.com/mandelsoft/logging"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/utils/blobaccess/wget"
+)
+
+type (
+ Options = wget.Options
+ Option = wget.Option
+)
+
+func WithCredentialContext(ctx credentials.ContextProvider) Option {
+ return wget.WithCredentialContext(ctx)
+}
+
+func WithLoggingContext(ctx logging.ContextProvider) Option {
+ return wget.WithLoggingContext(ctx)
+}
+
+func WithMimeType(mime string) Option {
+ return wget.WithMimeType(mime)
+}
+
+func WithCredentials(c credentials.Credentials) Option {
+ return wget.WithCredentials(c)
+}
+
+func WithHeader(h http.Header) Option {
+ return wget.WithHeader(h)
+}
+
+func WithVerb(v string) Option {
+ return wget.WithVerb(v)
+}
+
+func WithBody(v io.Reader) Option {
+ return wget.WithBody(v)
+}
+
+func WithNoRedirect(r ...bool) Option {
+ return wget.WithNoRedirect(r...)
+}
diff --git a/api/ocm/elements/artifactaccess/wgetaccess/resource.go b/api/ocm/elements/artifactaccess/wgetaccess/resource.go
new file mode 100644
index 000000000..56127b100
--- /dev/null
+++ b/api/ocm/elements/artifactaccess/wgetaccess/resource.go
@@ -0,0 +1,30 @@
+package wgetaccess
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactaccess/genericaccess"
+ access "ocm.software/ocm/api/ocm/extensions/accessmethods/wget"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+)
+
+const TYPE = resourcetypes.BLOB
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, url string, opts ...Option) cpi.ArtifactAccess[M] {
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ spec := access.New(url, opts...)
+ // is global access, must work, otherwise there is an error in the lib.
+ return genericaccess.MustAccess(ctx, meta, spec)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, url string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, url, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, url string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, url, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/api/options.go b/api/ocm/elements/artifactblob/api/options.go
new file mode 100644
index 000000000..442b77f48
--- /dev/null
+++ b/api/ocm/elements/artifactblob/api/options.go
@@ -0,0 +1,71 @@
+package api
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+type (
+ Option = optionutils.Option[*Options]
+ GeneralOptionsProvider = optionutils.NestedOptionsProvider[*Options]
+)
+
+type Options struct {
+ Global cpi.AccessSpec
+ Hint string
+}
+
+var (
+ _ optionutils.NestedOptionsProvider[*Options] = (*Options)(nil)
+ _ optionutils.Option[*Options] = (*Options)(nil)
+)
+
+func (w *Options) NestedOptions() *Options {
+ return w
+}
+
+func (o *Options) ApplyTo(opts *Options) {
+ if o.Global != nil {
+ opts.Global = o.Global
+ }
+ if o.Hint != "" {
+ opts.Hint = o.Hint
+ }
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+type hint string
+
+func (o hint) ApplyTo(opts *Options) {
+ opts.Hint = string(o)
+}
+
+func WithHint(h string) Option {
+ return hint(h)
+}
+
+func WrapHint[O any, P optionutils.OptionTargetProvider[*Options, O]](h string) optionutils.Option[P] {
+ return optionutils.OptionWrapper[*Options, O, P](WithHint(h))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type global struct {
+ cpi.AccessSpec
+}
+
+func (o global) ApplyTo(opts *Options) {
+ opts.Global = o.AccessSpec
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return global{a}
+}
+
+func WrapGlobalAccess[O any, P optionutils.OptionTargetProvider[*Options, O]](a cpi.AccessSpec) optionutils.Option[P] {
+ return optionutils.OptionWrapper[*Options, O, P](WithGlobalAccess(a))
+}
diff --git a/api/ocm/elements/artifactblob/datablob/options.go b/api/ocm/elements/artifactblob/datablob/options.go
new file mode 100644
index 000000000..487cd991b
--- /dev/null
+++ b/api/ocm/elements/artifactblob/datablob/options.go
@@ -0,0 +1,84 @@
+package datablob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+)
+
+type Option = optionutils.Option[*Options]
+
+type compressionMode string
+
+const (
+ COMPRESSION = compressionMode("compression")
+ DECOMPRESSION = compressionMode("decompression")
+ NONE = compressionMode("")
+)
+
+type Options struct {
+ api.Options
+ MimeType string
+ Compression compressionMode
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ if o.MimeType != "" {
+ opts.MimeType = o.MimeType
+ }
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Local Options
+
+type mimetype struct {
+ mime string
+}
+
+func (o mimetype) ApplyTo(opts *Options) {
+ opts.MimeType = o.mime
+}
+
+func WithMimeType(mime string) Option {
+ return mimetype{mime}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type compression struct {
+ mode compressionMode
+}
+
+func (o compression) ApplyTo(opts *Options) {
+ opts.Compression = o.mode
+}
+
+func WithCompression() Option {
+ return compression{COMPRESSION}
+}
+
+func WithDecompression() Option {
+ return compression{DECOMPRESSION}
+}
diff --git a/api/ocm/elements/artifactblob/datablob/resource.go b/api/ocm/elements/artifactblob/datablob/resource.go
new file mode 100644
index 000000000..2f5babc90
--- /dev/null
+++ b/api/ocm/elements/artifactblob/datablob/resource.go
@@ -0,0 +1,49 @@
+package datablob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, blob []byte, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+
+ media := eff.MimeType
+ if media == "" {
+ media = mime.MIME_OCTET
+ }
+
+ var blobprov blobaccess.BlobAccessProvider
+ switch eff.Compression {
+ case NONE:
+ blobprov = blobaccess.ProviderForData(media, blob)
+ case COMPRESSION:
+ blob := blobaccess.ForData(media, blob)
+ defer blob.Close()
+ blob, _ = blobaccess.WithCompression(blob)
+ blobprov = blobaccess.ProviderForBlobAccess(blob)
+ case DECOMPRESSION:
+ blob := blobaccess.ForData(media, blob)
+ defer blob.Close()
+ blob, _ = blobaccess.WithDecompression(blob)
+ blobprov = blobaccess.ProviderForBlobAccess(blob)
+ }
+
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, media string, meta *ocm.ResourceMeta, blob []byte, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, media string, meta *ocm.SourceMeta, blob []byte, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/dirtreeblob/options.go b/api/ocm/elements/artifactblob/dirtreeblob/options.go
new file mode 100644
index 000000000..642207e72
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dirtreeblob/options.go
@@ -0,0 +1,77 @@
+package dirtreeblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ base "ocm.software/ocm/api/utils/blobaccess/dirtree"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DirTree BlobAccess Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithFileSystem(fs vfs.FileSystem) Option {
+ return wrapBase(base.WithFileSystem(fs))
+}
+
+func WithExcludeFiles(files []string) Option {
+ return wrapBase(base.WithExcludeFiles(files))
+}
+
+func WithIncludeFiles(files []string) Option {
+ return wrapBase(base.WithIncludeFiles(files))
+}
+
+func WithFollowSymlinks(b ...bool) Option {
+ return wrapBase(base.WithFollowSymlinks(b...))
+}
+
+func WithPreserveDir(b ...bool) Option {
+ return wrapBase(base.WithPreserveDir(b...))
+}
+
+func WithCompressWithGzip(b ...bool) Option {
+ return wrapBase(base.WithCompressWithGzip(b...))
+}
diff --git a/api/ocm/elements/artifactblob/dirtreeblob/resource.go b/api/ocm/elements/artifactblob/dirtreeblob/resource.go
new file mode 100644
index 000000000..217046924
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dirtreeblob/resource.go
@@ -0,0 +1,33 @@
+package dirtreeblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/dirtree"
+)
+
+const TYPE = resourcetypes.DIRECTORY_TREE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, path string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+ blobprov := dirtree.Provider(path, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, path string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, path string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/dirtreeblob/resource_test.go b/api/ocm/elements/artifactblob/dirtreeblob/resource_test.go
new file mode 100644
index 000000000..5e49d9954
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dirtreeblob/resource_test.go
@@ -0,0 +1,75 @@
+package dirtreeblob_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ testenv "ocm.software/ocm/api/helper/env"
+ "ocm.software/ocm/api/ocm/compdesc"
+ me "ocm.software/ocm/api/ocm/elements/artifactblob/dirtreeblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/tarutils"
+)
+
+var _ = Describe("dir tree resource access", func() {
+ var env *testenv.Environment
+
+ BeforeEach(func() {
+ env = testenv.NewEnvironment(testenv.TestData())
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("creates resource", func() {
+ global := ociartifact.New("ghcr.io/mandelsoft/demo:v1.0.0")
+
+ acc := me.ResourceAccess(env.OCMContext(), compdesc.NewResourceMeta("test", "", compdesc.LocalRelation), "testdata",
+ me.WithExcludeFiles([]string{"dir/a"}),
+ me.WithFileSystem(env.FileSystem()),
+ me.WithHint("demo"),
+ me.WithGlobalAccess(global),
+ )
+
+ Expect(acc.ReferenceHint()).To(Equal("demo"))
+ Expect(acc.GlobalAccess()).To(Equal(global))
+ Expect(acc.Meta().Type).To(Equal(resourcetypes.DIRECTORY_TREE))
+
+ blob := Must(acc.BlobAccess())
+ defer Defer(blob.Close, "blob")
+ Expect(blob.MimeType()).To(Equal(mime.MIME_TAR))
+
+ r := Must(blob.Reader())
+ defer Defer(r.Close, "reader")
+ files := Must(tarutils.ListArchiveContentFromReader(r))
+ Expect(files).To(ConsistOf([]string{
+ "dir",
+ "dir/b",
+ "dir/c",
+ }))
+ })
+
+ It("adds resource", func() {
+ global := ociartifact.New("ghcr.io/mandelsoft/demo:v1.0.0")
+
+ acc := me.ResourceAccess(env.OCMContext(), compdesc.NewResourceMeta("test", "", compdesc.LocalRelation), "testdata",
+ me.WithExcludeFiles([]string{"dir/a"}),
+ me.WithFileSystem(env.FileSystem()),
+ me.WithHint("demo"),
+ me.WithGlobalAccess(global),
+ )
+
+ arch := Must(ctf.Create(env, accessobj.ACC_CREATE, "ctf", 0o700, env, accessobj.FormatDirectory))
+ c := Must(arch.LookupComponent("arcme.org/test"))
+ v := Must(c.NewVersion("v1.0.0"))
+
+ MustBeSuccessful(v.SetResourceByAccess(acc))
+ MustBeSuccessful(c.AddVersion(v))
+ })
+})
diff --git a/pkg/contexts/ocm/elements/artifactblob/dirtreeblob/suite_test.go b/api/ocm/elements/artifactblob/dirtreeblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/dirtreeblob/suite_test.go
rename to api/ocm/elements/artifactblob/dirtreeblob/suite_test.go
diff --git a/pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/a b/api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/a
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/a
rename to api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/a
diff --git a/pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/b b/api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/b
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/b
rename to api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/b
diff --git a/pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/c b/api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/c
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/dirtreeblob/testdata/dir/c
rename to api/ocm/elements/artifactblob/dirtreeblob/testdata/dir/c
diff --git a/pkg/contexts/ocm/elements/artifactblob/doc.go b/api/ocm/elements/artifactblob/doc.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/doc.go
rename to api/ocm/elements/artifactblob/doc.go
diff --git a/api/ocm/elements/artifactblob/dockerdaemonblob/options.go b/api/ocm/elements/artifactblob/dockerdaemonblob/options.go
new file mode 100644
index 000000000..60d1907f0
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dockerdaemonblob/options.go
@@ -0,0 +1,69 @@
+package dockerdaemonblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ base "ocm.software/ocm/api/utils/blobaccess/dockerdaemon"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Docker BlobAccess Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithName(n string) Option {
+ return wrapBase(base.WithName(n))
+}
+
+func WithVersion(v string) Option {
+ return wrapBase(base.WithVersion(v))
+}
+
+func WithVersionOverride(v string, flag ...bool) Option {
+ return wrapBase(base.WithVersionOverride(v, flag...))
+}
+
+func WithOrigin(o common.NameVersion) Option {
+ return wrapBase(base.WithOrigin(o))
+}
diff --git a/api/ocm/elements/artifactblob/dockerdaemonblob/resource.go b/api/ocm/elements/artifactblob/dockerdaemonblob/resource.go
new file mode 100644
index 000000000..33001bc89
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dockerdaemonblob/resource.go
@@ -0,0 +1,40 @@
+package dockerdaemonblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/dockerdaemon"
+)
+
+const TYPE = resourcetypes.OCI_IMAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, name string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+ eff.Blob.Context = ctx.OCIContext()
+ locator, version, err := dockerdaemon.ImageInfoFor(name, &eff.Blob)
+ if err == nil {
+ version = eff.Blob.Version
+ }
+ hint := ociartifact.Hint(optionutils.AsValue(eff.Blob.Origin), locator, eff.Hint, version)
+ blobprov := dockerdaemon.Provider(name, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, path string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, path string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/dockermultiblob/options.go b/api/ocm/elements/artifactblob/dockermultiblob/options.go
new file mode 100644
index 000000000..e7f5630a1
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dockermultiblob/options.go
@@ -0,0 +1,69 @@
+package dockermultiblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ base "ocm.software/ocm/api/utils/blobaccess/dockermulti"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Docker BlobAccess Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithVariants(names ...string) Option {
+ return wrapBase(base.WithVariants(names...))
+}
+
+func WithVersion(v string) Option {
+ return wrapBase(base.WithVersion(v))
+}
+
+func WithOrigin(o common.NameVersion) Option {
+ return wrapBase(base.WithOrigin(o))
+}
+
+func WithPrinter(p common.Printer) Option {
+ return wrapBase(base.WithPrinter(p))
+}
diff --git a/api/ocm/elements/artifactblob/dockermultiblob/resource.go b/api/ocm/elements/artifactblob/dockermultiblob/resource.go
new file mode 100644
index 000000000..9c58dd3bb
--- /dev/null
+++ b/api/ocm/elements/artifactblob/dockermultiblob/resource.go
@@ -0,0 +1,35 @@
+package dockermultiblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/dockermulti"
+)
+
+const TYPE = resourcetypes.OCI_IMAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+ eff.Blob.Context = ctx.OCIContext()
+
+ blobprov := dockermulti.Provider(&eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, name string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, opts...)
+}
diff --git a/pkg/contexts/ocm/elements/artifactblob/externalblob/doc.go b/api/ocm/elements/artifactblob/externalblob/doc.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/externalblob/doc.go
rename to api/ocm/elements/artifactblob/externalblob/doc.go
diff --git a/api/ocm/elements/artifactblob/externalblob/options.go b/api/ocm/elements/artifactblob/externalblob/options.go
new file mode 100644
index 000000000..74a51d1f9
--- /dev/null
+++ b/api/ocm/elements/artifactblob/externalblob/options.go
@@ -0,0 +1,22 @@
+package externalblob
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+)
+
+type (
+ Option = api.Option
+ Options = api.Options
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
diff --git a/api/ocm/elements/artifactblob/externalblob/resource.go b/api/ocm/elements/artifactblob/externalblob/resource.go
new file mode 100644
index 000000000..0bf8f45ac
--- /dev/null
+++ b/api/ocm/elements/artifactblob/externalblob/resource.go
@@ -0,0 +1,68 @@
+package externalblob
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, access ocm.AccessSpec, opts ...Option) (cpi.ArtifactAccess[M], error) {
+ eff := optionutils.EvalOptions(opts...)
+
+ hint := eff.Hint
+ if hint == "" {
+ hint = ocm.ReferenceHint(access, &cpi.DummyComponentVersionAccess{ctx})
+ }
+ global := eff.Global
+ if global == nil {
+ global = ocm.GlobalAccess(access, ctx)
+ }
+
+ prov, err := cpi.NewAccessProviderForExternalAccessSpec(ctx, access)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid external access method %q", access.GetKind())
+ }
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), newAccessProvider(prov, hint, global)), nil
+}
+
+type _accessProvider = cpi.AccessProvider
+
+type accessProvider struct {
+ _accessProvider
+ hint string
+ global cpi.AccessSpec
+}
+
+func newAccessProvider(prov cpi.AccessProvider, hint string, global cpi.AccessSpec) cpi.AccessProvider {
+ return &accessProvider{
+ _accessProvider: prov,
+ hint: hint,
+ global: global,
+ }
+}
+
+func (p *accessProvider) ReferenceHint() string {
+ if p.hint != "" {
+ return p.hint
+ }
+ return p._accessProvider.ReferenceHint()
+}
+
+func (p *accessProvider) GlobalAccess() cpi.AccessSpec {
+ if p.global != nil {
+ return p.global
+ }
+ return p._accessProvider.GlobalAccess()
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, access cpi.AccessSpec, opts ...Option) (cpi.ResourceAccess, error) {
+ return Access(ctx, meta, access, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, access cpi.AccessSpec, opts ...Option) (cpi.SourceAccess, error) {
+ return Access(ctx, meta, access, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/fileblob/options.go b/api/ocm/elements/artifactblob/fileblob/options.go
new file mode 100644
index 000000000..915a19f7e
--- /dev/null
+++ b/api/ocm/elements/artifactblob/fileblob/options.go
@@ -0,0 +1,85 @@
+package fileblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+)
+
+type Option = optionutils.Option[*Options]
+
+type compressionMode string
+
+const (
+ COMPRESSION = compressionMode("compression")
+ DECOMPRESSION = compressionMode("decompression")
+ NONE = compressionMode("")
+)
+
+type Options struct {
+ api.Options
+ FileSystem vfs.FileSystem
+ Compression compressionMode
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ if o.FileSystem != nil {
+ opts.FileSystem = o.FileSystem
+ }
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Local Options
+
+type filesystem struct {
+ fs vfs.FileSystem
+}
+
+func (o filesystem) ApplyTo(opts *Options) {
+ opts.FileSystem = o.fs
+}
+
+func WithFileSystem(fs vfs.FileSystem) Option {
+ return filesystem{fs}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type compression struct {
+ mode compressionMode
+}
+
+func (o compression) ApplyTo(opts *Options) {
+ opts.Compression = o.mode
+}
+
+func WithCompression() Option {
+ return compression{COMPRESSION}
+}
+
+func WithDecompression() Option {
+ return compression{DECOMPRESSION}
+}
diff --git a/api/ocm/elements/artifactblob/fileblob/resource.go b/api/ocm/elements/artifactblob/fileblob/resource.go
new file mode 100644
index 000000000..63587e05d
--- /dev/null
+++ b/api/ocm/elements/artifactblob/fileblob/resource.go
@@ -0,0 +1,54 @@
+package fileblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/blobaccess/file"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const TYPE = "blob"
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, media string, meta P, path string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+ if media == "" {
+ media = mime.MIME_OCTET
+ }
+
+ var blobprov blobaccess.BlobAccessProvider
+ switch eff.Compression {
+ case NONE:
+ blobprov = file.Provider(media, path, eff.FileSystem)
+ case COMPRESSION:
+ blob := file.BlobAccess(media, path, eff.FileSystem)
+ defer blob.Close()
+ blob, _ = blobaccess.WithCompression(blob)
+ blobprov = blobaccess.ProviderForBlobAccess(blob)
+ case DECOMPRESSION:
+ blob := file.BlobAccess(media, path, eff.FileSystem)
+ defer blob.Close()
+ blob, _ = blobaccess.WithDecompression(blob)
+ blobprov = blobaccess.ProviderForBlobAccess(blob)
+ }
+
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, media string, meta *ocm.ResourceMeta, path string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, media, meta, path, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, media string, meta *ocm.SourceMeta, path string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, media, meta, path, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/genericblob/options.go b/api/ocm/elements/artifactblob/genericblob/options.go
new file mode 100644
index 000000000..5fa0688ca
--- /dev/null
+++ b/api/ocm/elements/artifactblob/genericblob/options.go
@@ -0,0 +1,19 @@
+package genericblob
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+)
+
+type (
+ Options = api.Options
+ Option = api.Option
+)
+
+func WithHint(h string) Option {
+ return api.WithHint(h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WithGlobalAccess(a)
+}
diff --git a/api/ocm/elements/artifactblob/genericblob/resource.go b/api/ocm/elements/artifactblob/genericblob/resource.go
new file mode 100644
index 000000000..1bde5e9a4
--- /dev/null
+++ b/api/ocm/elements/artifactblob/genericblob/resource.go
@@ -0,0 +1,25 @@
+package genericblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx cpi.Context, meta P, blob blobaccess.BlobAccessProvider, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blob, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx cpi.Context, media string, meta *cpi.ResourceMeta, blob blobaccess.BlobAccessProvider, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
+
+func SourceAccess(ctx cpi.Context, media string, meta *cpi.SourceMeta, blob blobaccess.BlobAccessProvider, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/helmblob/helmblob_test.go b/api/ocm/elements/artifactblob/helmblob/helmblob_test.go
new file mode 100644
index 000000000..c30fdb7ca
--- /dev/null
+++ b/api/ocm/elements/artifactblob/helmblob/helmblob_test.go
@@ -0,0 +1,36 @@
+package helmblob_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "ocm.software/ocm/api/helper/env"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi"
+ me "ocm.software/ocm/api/ocm/elements/artifactblob/helmblob"
+ ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+var _ = Describe("", func() {
+ var e *Builder
+
+ BeforeEach(func() {
+ e = NewBuilder(env.TestData())
+ })
+
+ AfterEach(func() {
+ MustBeSuccessful(e.Cleanup())
+ })
+
+ It("", func() {
+ ctf := Must(ctfocm.Open(e, accessobj.ACC_CREATE, "/repo", 0o700, e, ctfocm.FormatDirectory))
+ defer Close(ctf)
+ cv := Must(ctf.NewComponentVersion("ocm.software/test-component", "1.0.0"))
+ defer Close(cv)
+ MustBeSuccessful(cv.SetResourceByAccess(me.ResourceAccess(e.OCMContext(), cpi.NewResourceMeta("helm1", "blob", metav1.LocalRelation), "/testdata/testchart1", me.WithFileSystem(e.FileSystem()))))
+ MustBeSuccessful(cv.SetResourceByAccess(me.ResourceAccess(e.OCMContext(), cpi.NewResourceMeta("helm2", "blob", metav1.LocalRelation), "/testdata/testchart2", me.WithFileSystem(e.FileSystem()))))
+ MustBeSuccessful(ctf.AddComponentVersion(cv, true))
+ })
+})
diff --git a/api/ocm/elements/artifactblob/helmblob/options.go b/api/ocm/elements/artifactblob/helmblob/options.go
new file mode 100644
index 000000000..97aa0f3a4
--- /dev/null
+++ b/api/ocm/elements/artifactblob/helmblob/options.go
@@ -0,0 +1,87 @@
+package helmblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ base "ocm.software/ocm/api/utils/blobaccess/helm"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DirTree BlobAccess Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithFileSystem(fs vfs.FileSystem) Option {
+ return wrapBase(base.WithFileSystem(fs))
+}
+
+func WithContext(ctx oci.ContextProvider) Option {
+ return wrapBase(base.WithContext(ctx))
+}
+
+func WithIVersion(v string) Option {
+ return wrapBase(base.WithVersion(v))
+}
+
+func WithIVersionOverride(v string, flag ...bool) Option {
+ return wrapBase(base.WithVersionOverride(v, flag...))
+}
+
+func WithCACert(v string) Option {
+ return wrapBase(base.WithCACert(v))
+}
+
+func WithCACertFile(v string) Option {
+ return wrapBase(base.WithCACertFile(v))
+}
+
+func WithHelmRepository(v string) Option {
+ return wrapBase(base.WithHelmRepository(v))
+}
+
+func WithPrinter(v common.Printer) Option {
+ return wrapBase(base.WithPrinter(v))
+}
diff --git a/api/ocm/elements/artifactblob/helmblob/resource.go b/api/ocm/elements/artifactblob/helmblob/resource.go
new file mode 100644
index 000000000..3b5a85910
--- /dev/null
+++ b/api/ocm/elements/artifactblob/helmblob/resource.go
@@ -0,0 +1,34 @@
+package helmblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/helm"
+)
+
+const TYPE = resourcetypes.HELM_CHART
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, path string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(append(opts, WithContext(ctx))...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+ hint := eff.Hint
+ blobprov := helm.Provider(path, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, path string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, path string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/suite_test.go b/api/ocm/elements/artifactblob/helmblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/suite_test.go
rename to api/ocm/elements/artifactblob/helmblob/suite_test.go
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/.helmignore b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/.helmignore
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/.helmignore
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/.helmignore
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/.idea/somefile b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/.idea/somefile
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/.idea/somefile
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/.idea/somefile
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/Chart.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/Chart.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/Chart.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/Chart.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/NOTES.txt b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/NOTES.txt
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/NOTES.txt
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/NOTES.txt
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/_helpers.tpl b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/_helpers.tpl
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/_helpers.tpl
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/_helpers.tpl
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/deployment.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/deployment.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/deployment.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/deployment.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/hpa.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/hpa.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/hpa.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/hpa.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/ingress.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/ingress.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/ingress.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/ingress.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/service.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/service.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/service.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/service.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/serviceaccount.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/serviceaccount.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/serviceaccount.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/serviceaccount.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/tests/test-connection.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/tests/test-connection.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/tests/test-connection.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/templates/tests/test-connection.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/values.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart1/values.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart1/values.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart1/values.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/.helmignore b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/.helmignore
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/.helmignore
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/.helmignore
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/.idea/somefile b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/.idea/somefile
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/.idea/somefile
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/.idea/somefile
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/Chart.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/Chart.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/Chart.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/Chart.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/NOTES.txt b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/NOTES.txt
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/NOTES.txt
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/NOTES.txt
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/_helpers.tpl b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/_helpers.tpl
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/_helpers.tpl
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/_helpers.tpl
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/deployment.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/deployment.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/deployment.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/deployment.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/hpa.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/hpa.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/hpa.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/hpa.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/ingress.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/ingress.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/ingress.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/ingress.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/service.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/service.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/service.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/service.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/serviceaccount.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/serviceaccount.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/serviceaccount.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/serviceaccount.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/tests/test-connection.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/tests/test-connection.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/tests/test-connection.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/templates/tests/test-connection.yaml
diff --git a/pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/values.yaml b/api/ocm/elements/artifactblob/helmblob/testdata/testchart2/values.yaml
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/helmblob/testdata/testchart2/values.yaml
rename to api/ocm/elements/artifactblob/helmblob/testdata/testchart2/values.yaml
diff --git a/api/ocm/elements/artifactblob/mavenblob/access_test.go b/api/ocm/elements/artifactblob/mavenblob/access_test.go
new file mode 100644
index 000000000..c2810e18c
--- /dev/null
+++ b/api/ocm/elements/artifactblob/mavenblob/access_test.go
@@ -0,0 +1,71 @@
+package mavenblob_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "ocm.software/ocm/api/ocm/elements"
+ me "ocm.software/ocm/api/ocm/elements/artifactblob/mavenblob"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/repositories/composition"
+ "ocm.software/ocm/api/tech/maven"
+ "ocm.software/ocm/api/tech/maven/maventest"
+)
+
+const (
+ MAVEN_PATH = "/testdata/.m2/repository"
+ FAIL_PATH = "/testdata/.m2/fail"
+ MAVEN_CENTRAL_ADDRESS = "repo.maven.apache.org:443"
+ MAVEN_CENTRAL = "https://repo.maven.apache.org/maven2/"
+ MAVEN_GROUP_ID = "maven"
+ MAVEN_ARTIFACT_ID = "maven"
+ MAVEN_VERSION = "1.1"
+)
+
+var _ = Describe("blobaccess for maven", func() {
+ Context("maven filesystem repository", func() {
+ var env *Builder
+ var repo *maven.Repository
+
+ BeforeEach(func() {
+ env = NewBuilder(maventest.TestData())
+ repo = maven.NewFileRepository(MAVEN_PATH, env.FileSystem())
+ })
+
+ AfterEach(func() {
+ MustBeSuccessful(env.Cleanup())
+ })
+
+ It("blobaccess for a single file with classifier and extension", func() {
+ cv := composition.NewComponentVersion(env.OCMContext(), "acme.org/test", "1.0.0")
+ defer Close(cv)
+
+ coords := maven.NewCoordinates("com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0",
+ maven.WithClassifier("random-content"), maven.WithExtension("json"))
+
+ a := me.ResourceAccessForMavenCoords(env.OCMContext(), Must(elements.ResourceMeta("mavenblob", resourcetypes.OCM_JSON, elements.WithLocalRelation())), repo, coords, me.WithCachingFileSystem(env.FileSystem()))
+ Expect(a.ReferenceHint()).To(Equal(""))
+ b := Must(a.BlobAccess())
+ defer Close(b)
+ Expect(string(Must(b.Get()))).To(Equal(`{"some": "test content"}`))
+
+ MustBeSuccessful(cv.SetResourceByAccess(a))
+ r := Must(cv.GetResourceByIndex(0))
+ m := Must(r.AccessMethod())
+ defer Close(m)
+ Expect(string(Must(m.Get()))).To(Equal(`{"some": "test content"}`))
+ })
+
+ It("blobaccess for package", func() {
+ cv := composition.NewComponentVersion(env.OCMContext(), "acme.org/test", "1.0.0")
+ defer Close(cv)
+
+ coords := maven.NewCoordinates("com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0")
+
+ a := me.ResourceAccessForMavenCoords(env.OCMContext(), Must(elements.ResourceMeta("mavenblob", resourcetypes.OCM_JSON, elements.WithLocalRelation())), repo, coords, me.WithCachingFileSystem(env.FileSystem()))
+ Expect(a.ReferenceHint()).To(Equal(coords.GAV()))
+ })
+ })
+})
diff --git a/api/ocm/elements/artifactblob/mavenblob/options.go b/api/ocm/elements/artifactblob/mavenblob/options.go
new file mode 100644
index 000000000..ae05fd95e
--- /dev/null
+++ b/api/ocm/elements/artifactblob/mavenblob/options.go
@@ -0,0 +1,104 @@
+package mavenblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/mandelsoft/logging"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ "ocm.software/ocm/api/tech/maven"
+ base "ocm.software/ocm/api/utils/blobaccess/maven"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithHintForCoords(coords *maven.Coordinates) Option {
+ if coords.IsPackage() {
+ return WithHint(coords.GAV())
+ }
+ return optionutils.NoOption[*Options]{}
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Local Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithCredentialContext(credctx credentials.ContextProvider) Option {
+ return wrapBase(base.WithCredentialContext(credctx))
+}
+
+func WithLoggingContext(logctx logging.ContextProvider) Option {
+ return wrapBase(base.WithLoggingContext(logctx))
+}
+
+func WithCachingContext(cachectx datacontext.Context) Option {
+ return wrapBase(base.WithCachingContext(cachectx))
+}
+
+func WithCachingFileSystem(fs vfs.FileSystem) Option {
+ return wrapBase(base.WithCachingFileSystem(fs))
+}
+
+func WithCachingPath(p string) Option {
+ return wrapBase(base.WithCachingPath(p))
+}
+
+func WithCredentials(c credentials.Credentials) Option {
+ return wrapBase(base.WithCredentials(c))
+}
+
+func WithClassifier(c string) Option {
+ return wrapBase(base.WithClassifier(c))
+}
+
+func WithOptionalClassifier(c *string) Option {
+ return wrapBase(base.WithOptionalClassifier(c))
+}
+
+func WithExtension(e string) Option {
+ return wrapBase(base.WithExtension(e))
+}
+
+func WithOptionalExtension(e *string) Option {
+ return wrapBase(base.WithOptionalExtension(e))
+}
diff --git a/api/ocm/elements/artifactblob/mavenblob/resource.go b/api/ocm/elements/artifactblob/mavenblob/resource.go
new file mode 100644
index 000000000..b30878cee
--- /dev/null
+++ b/api/ocm/elements/artifactblob/mavenblob/resource.go
@@ -0,0 +1,46 @@
+package mavenblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/maven"
+)
+
+const TYPE = resourcetypes.MAVEN_PACKAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, repo *maven.Repository, groupId, artifactId, version string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(optionutils.WithDefaults(opts, WithCredentialContext(ctx))...)
+ if eff.Blob.IsPackage() && eff.Hint == "" {
+ eff.Hint = maven.NewCoordinates(groupId, artifactId, version).GAV()
+ }
+
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ blobprov := maven.Provider(repo, groupId, artifactId, version, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *ocm.ResourceMeta, repo *maven.Repository, groupId, artifactId, version string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, repo, groupId, artifactId, version, opts...)
+}
+
+func ResourceAccessForMavenCoords(ctx ocm.Context, meta *ocm.ResourceMeta, repo *maven.Repository, coords *maven.Coordinates, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, repo, coords.GroupId, coords.ArtifactId, coords.Version, optionutils.WithDefaults(opts, WithOptionalClassifier(coords.Classifier), WithOptionalExtension(coords.Extension))...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *ocm.SourceMeta, repo *maven.Repository, groupId, artifactId, version string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, repo, groupId, artifactId, version, opts...)
+}
+
+func SourceAccessForMavenCoords(ctx ocm.Context, meta *ocm.SourceMeta, repo *maven.Repository, coords *maven.Coordinates, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, repo, coords.GroupId, coords.ArtifactId, coords.Version, optionutils.WithDefaults(opts, WithOptionalClassifier(coords.Classifier), WithOptionalExtension(coords.Extension))...)
+}
diff --git a/pkg/contexts/ocm/elements/artifactblob/mavenblob/suite_test.go b/api/ocm/elements/artifactblob/mavenblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifactblob/mavenblob/suite_test.go
rename to api/ocm/elements/artifactblob/mavenblob/suite_test.go
diff --git a/api/ocm/elements/artifactblob/ociartifactblob/options.go b/api/ocm/elements/artifactblob/ociartifactblob/options.go
new file mode 100644
index 000000000..65a4fe6d0
--- /dev/null
+++ b/api/ocm/elements/artifactblob/ociartifactblob/options.go
@@ -0,0 +1,66 @@
+package ociartifactblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ base "ocm.software/ocm/api/utils/blobaccess/ociartifact"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DirTree BlobAccess Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithContext(ctx oci.ContextProvider) Option {
+ return wrapBase(base.WithContext(ctx))
+}
+
+func WithVersion(v string) Option {
+ return wrapBase(base.WithVersion(v))
+}
+
+func WithPrinter(v common.Printer) Option {
+ return wrapBase(base.WithPrinter(v))
+}
diff --git a/api/ocm/elements/artifactblob/ociartifactblob/resource.go b/api/ocm/elements/artifactblob/ociartifactblob/resource.go
new file mode 100644
index 000000000..48c848f36
--- /dev/null
+++ b/api/ocm/elements/artifactblob/ociartifactblob/resource.go
@@ -0,0 +1,43 @@
+package ociartifactblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ blob "ocm.software/ocm/api/utils/blobaccess/ociartifact"
+)
+
+const TYPE = resourcetypes.OCI_IMAGE
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, refname string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(append(opts, WithContext(ctx))...)
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ hint := eff.Hint
+ if hint == "" {
+ ref, err := oci.ParseRef(refname)
+ if err == nil {
+ hint = ref.String()
+ }
+ }
+
+ blobprov := blob.Provider(refname, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *cpi.ResourceMeta, path string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *cpi.SourceMeta, path string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, path, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/textblob/options.go b/api/ocm/elements/artifactblob/textblob/options.go
new file mode 100644
index 000000000..145c9f0ba
--- /dev/null
+++ b/api/ocm/elements/artifactblob/textblob/options.go
@@ -0,0 +1,43 @@
+package textblob
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/datablob"
+)
+
+type (
+ Option = datablob.Option
+ Options = datablob.Options
+)
+
+const (
+ COMPRESSION = datablob.COMPRESSION
+ DECOMPRESSION = datablob.DECOMPRESSION
+ NONE = datablob.NONE
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return datablob.WithHint(h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return datablob.WithGlobalAccess(a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Local Options
+
+func WithMimeType(mime string) Option {
+ return datablob.WithMimeType(mime)
+}
+
+func WithCompression() Option {
+ return datablob.WithCompression()
+}
+
+func WithDecompression() Option {
+ return datablob.WithDecompression()
+}
diff --git a/api/ocm/elements/artifactblob/textblob/resource.go b/api/ocm/elements/artifactblob/textblob/resource.go
new file mode 100644
index 000000000..7b20ff68d
--- /dev/null
+++ b/api/ocm/elements/artifactblob/textblob/resource.go
@@ -0,0 +1,27 @@
+package textblob
+
+import (
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/datablob"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, blob string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(opts...)
+ if eff.MimeType == "" {
+ eff.MimeType = mime.MIME_TEXT
+ }
+ return datablob.Access(ctx, meta, []byte(blob), eff)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *ocm.ResourceMeta, blob string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *ocm.SourceMeta, blob string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, blob, opts...)
+}
diff --git a/api/ocm/elements/artifactblob/wgetblob/options.go b/api/ocm/elements/artifactblob/wgetblob/options.go
new file mode 100644
index 000000000..cf17a570f
--- /dev/null
+++ b/api/ocm/elements/artifactblob/wgetblob/options.go
@@ -0,0 +1,90 @@
+package wgetblob
+
+import (
+ "io"
+ "net/http"
+
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/mandelsoft/logging"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/elements/artifactblob/api"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/wget"
+ base "ocm.software/ocm/api/utils/blobaccess/wget"
+)
+
+type Option = optionutils.Option[*Options]
+
+type Options struct {
+ api.Options
+ Blob base.Options
+}
+
+var (
+ _ api.GeneralOptionsProvider = (*Options)(nil)
+ _ Option = (*Options)(nil)
+)
+
+func (o *Options) ApplyTo(opts *Options) {
+ o.Options.ApplyTo(&opts.Options)
+ o.Blob.ApplyTo(&opts.Blob)
+}
+
+func (o *Options) Apply(opts ...Option) {
+ optionutils.ApplyOptions(o, opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// General Options
+
+func WithHint(h string) Option {
+ return api.WrapHint[Options](h)
+}
+
+func WithGlobalAccess(a cpi.AccessSpec) Option {
+ return api.WrapGlobalAccess[Options](a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Local Options
+
+func mapBaseOption(opts *Options) *base.Options {
+ return &opts.Blob
+}
+
+func wrapBase(o base.Option) Option {
+ return optionutils.OptionWrapperFunc[*base.Options, *Options](o, mapBaseOption)
+}
+
+func WithCredentialContext(credctx credentials.ContextProvider) Option {
+ return wrapBase(base.WithCredentialContext(credctx))
+}
+
+func WithLoggingContext(logctx logging.ContextProvider) Option {
+ return wrapBase(base.WithLoggingContext(logctx))
+}
+
+func WithMimeType(mime string) Option {
+ return wrapBase(base.WithMimeType(mime))
+}
+
+func WithCredentials(creds credentials.Credentials) Option {
+ return wrapBase(base.WithCredentials(creds))
+}
+
+func WithHeader(h http.Header) Option {
+ return wrapBase(base.WithHeader(h))
+}
+
+func WithVerb(v string) Option {
+ return wrapBase(base.WithVerb(v))
+}
+
+func WithBody(v io.Reader) Option {
+ return wrapBase(base.WithBody(v))
+}
+
+func WithNoRedirect(r ...bool) Option {
+ return wrapBase(wget.WithNoRedirect(r...))
+}
diff --git a/api/ocm/elements/artifactblob/wgetblob/resource.go b/api/ocm/elements/artifactblob/wgetblob/resource.go
new file mode 100644
index 000000000..02306f06d
--- /dev/null
+++ b/api/ocm/elements/artifactblob/wgetblob/resource.go
@@ -0,0 +1,35 @@
+package wgetblob
+
+import (
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/blobaccess/wget"
+)
+
+const TYPE = resourcetypes.BLOB
+
+func Access[M any, P compdesc.ArtifactMetaPointer[M]](ctx ocm.Context, meta P, url string, opts ...Option) cpi.ArtifactAccess[M] {
+ eff := optionutils.EvalOptions(optionutils.WithDefaults(opts, WithCredentialContext(ctx))...)
+
+ if meta.GetType() == "" {
+ meta.SetType(TYPE)
+ }
+
+ blobprov := wget.Provider(url, &eff.Blob)
+ accprov := cpi.NewAccessProviderForBlobAccessProvider(ctx, blobprov, eff.Hint, eff.Global)
+ // strange type cast is required by Go compiler, meta has the correct type.
+ return cpi.NewArtifactAccessForProvider(generics.Cast[*M](meta), accprov)
+}
+
+func ResourceAccess(ctx ocm.Context, meta *ocm.ResourceMeta, url string, opts ...Option) cpi.ResourceAccess {
+ return Access(ctx, meta, url, opts...)
+}
+
+func SourceAccess(ctx ocm.Context, meta *ocm.SourceMeta, url string, opts ...Option) cpi.SourceAccess {
+ return Access(ctx, meta, url, opts...)
+}
diff --git a/pkg/contexts/ocm/elements/artifacts.go b/api/ocm/elements/artifacts.go
similarity index 100%
rename from pkg/contexts/ocm/elements/artifacts.go
rename to api/ocm/elements/artifacts.go
diff --git a/pkg/contexts/ocm/elements/common.go b/api/ocm/elements/common.go
similarity index 93%
rename from pkg/contexts/ocm/elements/common.go
rename to api/ocm/elements/common.go
index c4321bff1..1b210eeab 100644
--- a/pkg/contexts/ocm/elements/common.go
+++ b/api/ocm/elements/common.go
@@ -1,8 +1,8 @@
package elements
import (
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
)
type CommonOption interface {
diff --git a/pkg/contexts/ocm/elements/digests.go b/api/ocm/elements/digests.go
similarity index 84%
rename from pkg/contexts/ocm/elements/digests.go
rename to api/ocm/elements/digests.go
index 2a80de0eb..4aa372761 100644
--- a/pkg/contexts/ocm/elements/digests.go
+++ b/api/ocm/elements/digests.go
@@ -1,8 +1,8 @@
package elements
import (
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
)
type ResourceReferenceOption interface {
diff --git a/pkg/contexts/ocm/elements/doc.go b/api/ocm/elements/doc.go
similarity index 100%
rename from pkg/contexts/ocm/elements/doc.go
rename to api/ocm/elements/doc.go
diff --git a/api/ocm/elements/references.go b/api/ocm/elements/references.go
new file mode 100644
index 000000000..64bb1619f
--- /dev/null
+++ b/api/ocm/elements/references.go
@@ -0,0 +1,22 @@
+package elements
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm/compdesc"
+)
+
+type ReferenceOption interface {
+ ApplyToReference(reference *compdesc.ComponentReference) error
+}
+
+func Reference(name, comp, vers string, opts ...ReferenceOption) (*compdesc.ComponentReference, error) {
+ m := compdesc.NewComponentReference(name, comp, vers, nil)
+ list := errors.ErrList()
+ for _, o := range opts {
+ if o != nil {
+ list.Add(o.ApplyToReference(m))
+ }
+ }
+ return m, list.Result()
+}
diff --git a/api/ocm/elements/resources.go b/api/ocm/elements/resources.go
new file mode 100644
index 000000000..1938877f0
--- /dev/null
+++ b/api/ocm/elements/resources.go
@@ -0,0 +1,78 @@
+package elements
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/utils"
+)
+
+type ResourceMetaOption interface {
+ ApplyToResourceMeta(*compdesc.ResourceMeta) error
+}
+
+func ResourceMeta(name, typ string, opts ...ResourceMetaOption) (*compdesc.ResourceMeta, error) {
+ m := compdesc.NewResourceMeta(name, typ, metav1.LocalRelation)
+ list := errors.ErrList()
+ for _, o := range opts {
+ if o != nil {
+ list.Add(o.ApplyToResourceMeta(m))
+ }
+ }
+ return m, list.Result()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type local bool
+
+func (o local) ApplyToResourceMeta(m *compdesc.ResourceMeta) error {
+ if o {
+ m.Relation = metav1.LocalRelation
+ } else {
+ m.Relation = metav1.ExternalRelation
+ }
+ return nil
+}
+
+// WithLocalRelation sets the resource relation to metav1.LocalRelation.
+func WithLocalRelation(flag ...bool) ResourceMetaOption {
+ return local(utils.OptionalDefaultedBool(true, flag...))
+}
+
+// WithExternalRelation sets the resource relation to metav1.ExternalRelation.
+func WithExternalRelation(flag ...bool) ResourceMetaOption {
+ return local(!utils.OptionalDefaultedBool(true, flag...))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type srcref struct {
+ ref metav1.StringMap
+ labels metav1.Labels
+ errlist errors.ErrorList
+}
+
+var _ ResourceMetaOption = (*srcref)(nil)
+
+func (o *srcref) ApplyToResourceMeta(m *compdesc.ResourceMeta) error {
+ if err := o.errlist.Result(); err != nil {
+ return err
+ }
+ m.SourceRefs = append(m.SourceRefs, compdesc.SourceRef{IdentitySelector: o.ref.Copy(), Labels: o.labels.Copy()})
+ return nil
+}
+
+func (o *srcref) WithLabel(name string, value interface{}, opts ...metav1.LabelOption) *srcref {
+ r := &srcref{ref: o.ref, labels: o.labels.Copy()}
+ r.errlist.Add(r.labels.Set(name, value, opts...))
+ return r
+}
+
+// WithSourceRef adds a source reference to a resource meta object.
+// this is a sequence of name/value pairs.
+// Optionally, additional labels can be added with srcref.WithLabel.
+func WithSourceRef(sel ...string) *srcref {
+ return &srcref{ref: metav1.StringMap(metav1.NewExtraIdentity(sel...))}
+}
diff --git a/pkg/contexts/ocm/elements/resources_test.go b/api/ocm/elements/resources_test.go
similarity index 77%
rename from pkg/contexts/ocm/elements/resources_test.go
rename to api/ocm/elements/resources_test.go
index 002398fac..33c37829c 100644
--- a/pkg/contexts/ocm/elements/resources_test.go
+++ b/api/ocm/elements/resources_test.go
@@ -5,10 +5,10 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/digester/digesters/blob"
- me "github.com/open-component-model/ocm/pkg/contexts/ocm/elements"
- "github.com/open-component-model/ocm/pkg/signing/hasher/sha256"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ me "ocm.software/ocm/api/ocm/elements"
+ "ocm.software/ocm/api/ocm/extensions/digester/digesters/blob"
+ "ocm.software/ocm/api/tech/signing/hasher/sha256"
)
type value struct {
diff --git a/pkg/contexts/ocm/elements/sources.go b/api/ocm/elements/sources.go
similarity index 86%
rename from pkg/contexts/ocm/elements/sources.go
rename to api/ocm/elements/sources.go
index 22f7e1292..c4b053ab1 100644
--- a/pkg/contexts/ocm/elements/sources.go
+++ b/api/ocm/elements/sources.go
@@ -3,7 +3,7 @@ package elements
import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/compdesc"
)
type SourceMetaOption interface {
diff --git a/pkg/contexts/ocm/elements/suite_test.go b/api/ocm/elements/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/elements/suite_test.go
rename to api/ocm/elements/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/compose/method.go b/api/ocm/extensions/accessmethods/compose/method.go
new file mode 100644
index 000000000..9228d2b1c
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/compose/method.go
@@ -0,0 +1,145 @@
+package compose
+
+import (
+ "fmt"
+ "io"
+ "sync/atomic"
+
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of GitHub registry.
+const (
+ Type = "compose"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func Is(spec accspeccpi.AccessSpec) bool {
+ return spec != nil && spec.GetKind() == Type
+}
+
+// AccessSpec describes the access for a GitHub registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // Id is the internal id to identify the content
+ Id string `json:"id"`
+
+ // MediaType is the media type of the object represented by the blob
+ MediaType string `json:"mediaType"`
+
+ // GlobalAccess is an optional field describing a possibility
+ // for a global access. If given, it MUST describe a global access method.
+ GlobalAccess *accspeccpi.AccessSpecRef `json:"globalAccess,omitempty"`
+ // ReferenceName is an optional static name the object should be
+ // use in a local repository context. It is use by a repository
+ // to optionally determine a globally referencable access according
+ // to the OCI distribution spec. The result will be stored
+ // by the repository in the field ImageReference.
+ // The value is typically an OCI repository name optionally
+ // followed by a colon ':' and a tag
+ ReferenceName string `json:"referenceName,omitempty"`
+}
+
+var (
+ _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+ _ accspeccpi.HintProvider = (*AccessSpec)(nil)
+ _ accspeccpi.GlobalAccessProvider = (*AccessSpec)(nil)
+)
+
+// New creates a new GitHub registry access spec version v1.
+func New(hint string, mediaType string, global accspeccpi.AccessSpec) *AccessSpec {
+ id := fmt.Sprintf("compose-%d", number.Add(1))
+ s := &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ Id: id,
+ ReferenceName: hint,
+ MediaType: mediaType,
+ GlobalAccess: accspeccpi.NewAccessSpecRef(global),
+ }
+ return s
+}
+
+var number atomic.Int64
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("Composition blob %s", a.Id)
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return true
+}
+
+func (a *AccessSpec) GetReferenceHint(cv accspeccpi.ComponentVersionAccess) string {
+ return a.ReferenceName
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ if g, err := ctx.AccessSpecForSpec(a.GlobalAccess); err == nil {
+ return g
+ }
+ return a.GlobalAccess.Unwrap()
+}
+
+func (_ *AccessSpec) GetType() string {
+ return Type
+}
+
+func (a *AccessSpec) AccessMethod(cv accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return cv.AccessMethod(a)
+}
+
+type accessMethod struct {
+ access blobaccess.BlobAccess
+
+ spec *AccessSpec
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func NewMethod(spec *AccessSpec, blob blobaccess.BlobAccess) (accspeccpi.AccessMethod, error) {
+ if blob.MimeType() != spec.MediaType {
+ return nil, fmt.Errorf("mimetype mismatch (spec=%s, blob=%s)", spec.MediaType, blob.MimeType())
+ }
+ b, err := blob.Dup()
+ if err != nil {
+ return nil, err
+ }
+ return accspeccpi.AccessMethodForImplementation(&accessMethod{
+ access: b,
+ spec: spec,
+ }, nil)
+}
+
+func (_ *accessMethod) IsLocal() bool {
+ return true
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) MimeType() string {
+ return m.access.MimeType()
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ return m.access.Get()
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ return m.access.Reader()
+}
+
+func (m *accessMethod) Close() error {
+ if m.access == nil {
+ return nil
+ }
+ return m.access.Close()
+}
diff --git a/pkg/contexts/ocm/accessmethods/github/README.md b/api/ocm/extensions/accessmethods/github/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/github/README.md
rename to api/ocm/extensions/accessmethods/github/README.md
diff --git a/api/ocm/extensions/accessmethods/github/cli.go b/api/ocm/extensions/accessmethods/github/cli.go
new file mode 100644
index 000000000..9b39bbdce
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/github/cli.go
@@ -0,0 +1,43 @@
+package github
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.RepositoryOption,
+ options.HostnameOption,
+ options.CommitOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.RepositoryOption, config, "repoUrl")
+ flagsets.AddFieldByOptionP(opts, options.CommitOption, config, "commit")
+ flagsets.AddFieldByOptionP(opts, options.HostnameOption, config, "apiHostname")
+ return nil
+}
+
+var usage = `
+This method implements the access of the content of a git commit stored in a
+GitHub repository.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **repoUrl
** *string*
+
+ Repository URL with or without scheme.
+
+- **ref
** (optional) *string*
+
+ Original ref used to get the commit from
+
+- **commit
** *string*
+
+ The sha/id of the git commit
+`
diff --git a/api/ocm/extensions/accessmethods/github/method.go b/api/ocm/extensions/accessmethods/github/method.go
new file mode 100644
index 000000000..886a7536f
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/github/method.go
@@ -0,0 +1,314 @@
+package github
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "unicode"
+
+ "github.com/google/go-github/v45/github"
+ "github.com/mandelsoft/goutils/errors"
+ "golang.org/x/oauth2"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/github/identity"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessio/downloader"
+ hd "ocm.software/ocm/api/utils/accessio/downloader/http"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of GitHub registry.
+const (
+ Type = "gitHub"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+const (
+ LegacyType = "github"
+ LegacyTypeV1 = LegacyType + runtime.VersionSeparator + "v1"
+)
+
+const ShaLength = 40
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyTypeV1))
+}
+
+func Is(spec accspeccpi.AccessSpec) bool {
+ return spec != nil && spec.GetKind() == Type || spec.GetKind() == LegacyType
+}
+
+// AccessSpec describes the access for a GitHub registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // RepoUrl is the repository URL, with host, owner and repository
+ RepoURL string `json:"repoUrl"`
+
+ // APIHostname is an optional different hostname for accessing the GitHub REST API
+ // for enterprise installations
+ APIHostname string `json:"apiHostname,omitempty"`
+
+ // Commit defines the hash of the commit
+ Commit string `json:"commit"`
+
+ client *http.Client
+ downloader downloader.Downloader
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+// AccessSpecOptions defines a set of options which can be applied to the access spec.
+type AccessSpecOptions func(s *AccessSpec)
+
+// WithClient creates an access spec with a custom http client.
+func WithClient(client *http.Client) AccessSpecOptions {
+ return func(s *AccessSpec) {
+ s.client = client
+ }
+}
+
+// WithDownloader defines a client with a custom downloader.
+func WithDownloader(downloader downloader.Downloader) AccessSpecOptions {
+ return func(s *AccessSpec) {
+ s.downloader = downloader
+ }
+}
+
+// New creates a new GitHub registry access spec version v1.
+func New(repoURL, apiHostname, commit string, opts ...AccessSpecOptions) *AccessSpec {
+ s := &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ RepoURL: repoURL,
+ APIHostname: apiHostname,
+ Commit: commit,
+ }
+ for _, o := range opts {
+ o(s)
+ }
+ return s
+}
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("GitHub commit %s[%s]", a.RepoURL, a.Commit)
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+func (_ *AccessSpec) GetType() string {
+ return Type
+}
+
+func (a *AccessSpec) AccessMethod(c accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(newMethod(c, a))
+}
+
+func (a *AccessSpec) createHTTPClient(token string) *http.Client {
+ if token != "" {
+ ts := oauth2.StaticTokenSource(
+ &oauth2.Token{AccessToken: token},
+ )
+ ctx := context.Background()
+ // set up the test client if we have one
+ if a.client != nil {
+ ctx = context.WithValue(ctx, oauth2.HTTPClient, a.client)
+ }
+ return oauth2.NewClient(ctx, ts)
+ }
+ return a.client
+}
+
+// RepositoryService defines capabilities of a GitHub repository.
+type RepositoryService interface {
+ GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, followRedirects bool) (*url.URL, *github.Response, error)
+}
+
+type accessMethod struct {
+ lock sync.Mutex
+ access blobaccess.BlobAccess
+
+ compvers accspeccpi.ComponentVersionAccess
+ spec *AccessSpec
+ repositoryService RepositoryService
+ owner string
+ repo string
+ cid credentials.ConsumerIdentity
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (*accessMethod, error) {
+ if err := validateCommit(a.Commit); err != nil {
+ return nil, fmt.Errorf("failed to validate commit: %w", err)
+ }
+
+ unparsed := a.RepoURL
+ if !strings.HasPrefix(unparsed, "https://") && !strings.HasPrefix(unparsed, "http://") {
+ unparsed = "https://" + unparsed
+ }
+ u, err := url.Parse(unparsed)
+ if err != nil {
+ return nil, errors.ErrInvalidWrap(err, "repository url", a.RepoURL)
+ }
+
+ path := strings.Trim(u.Path, "/")
+ pathcomps := strings.Split(path, "/")
+ if len(pathcomps) != 2 {
+ return nil, errors.ErrInvalid("repository path", path, a.RepoURL)
+ }
+
+ token, cid, err := getCreds(unparsed, path, c.GetContext().CredentialsContext())
+ if err != nil {
+ return nil, fmt.Errorf("failed to get creds: %w", err)
+ }
+
+ var client *github.Client
+ httpclient := a.createHTTPClient(token)
+
+ if u.Hostname() == "github.com" {
+ client = github.NewClient(httpclient)
+ } else {
+ t := *u
+ t.Path = ""
+ if a.APIHostname != "" {
+ t.Host = a.APIHostname
+ }
+
+ client, err = github.NewEnterpriseClient(t.String(), t.String(), httpclient)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &accessMethod{
+ spec: a,
+ compvers: c,
+ owner: pathcomps[0],
+ repo: pathcomps[1],
+ cid: cid,
+ repositoryService: client.Repositories,
+ }, nil
+}
+
+func validateCommit(commit string) error {
+ if len(commit) != ShaLength {
+ return fmt.Errorf("commit is not a SHA")
+ }
+ for _, c := range commit {
+ if !unicode.IsOneOf([]*unicode.RangeTable{unicode.Letter, unicode.Digit}, c) {
+ return fmt.Errorf("commit contains invalid characters for a SHA")
+ }
+ }
+ return nil
+}
+
+func getCreds(serverurl, path string, cctx credentials.Context) (string, credentials.ConsumerIdentity, error) {
+ id := identity.GetConsumerId(serverurl, path)
+ creds, err := credentials.CredentialsForConsumer(cctx.CredentialsContext(), id, identity.IdentityMatcher)
+ if creds == nil || err != nil {
+ return "", id, err
+ }
+ return creds.GetProperty(credentials.ATTR_TOKEN), id, nil
+}
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) MimeType() string {
+ return mime.MIME_TGZ
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ if err := m.setup(); err != nil {
+ return nil, err
+ }
+ return m.access.Get()
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ if err := m.setup(); err != nil {
+ return nil, err
+ }
+ return m.access.Reader()
+}
+
+func (m *accessMethod) Close() error {
+ if m.access == nil {
+ return nil
+ }
+ return m.access.Close()
+}
+
+func (m *accessMethod) setup() error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.access != nil {
+ return nil
+ }
+
+ // to get the download link technical access to the github repo is required.
+ // therefore, this part has to be delayed until the effective access and cannot
+ // be done during creation of the access method object.
+ link, err := m.getDownloadLink()
+ if err != nil {
+ return fmt.Errorf("failed to get download link: %w", err)
+ }
+
+ d := hd.NewDownloader(link)
+ if m.spec.downloader != nil {
+ d = m.spec.downloader
+ }
+
+ w := accessio.NewWriteAtWriter(d.Download)
+ cacheBlobAccess := accessobj.CachedBlobAccessForWriter(m.compvers.GetContext(), m.MimeType(), w)
+ m.access = cacheBlobAccess
+ return nil
+}
+
+func (m *accessMethod) getDownloadLink() (string, error) {
+ link, resp, err := m.repositoryService.GetArchiveLink(context.Background(), m.owner, m.repo, github.Tarball, &github.RepositoryContentGetOptions{
+ Ref: m.spec.Commit,
+ }, true)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ return link.String(), nil
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ return m.cid
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
diff --git a/api/ocm/extensions/accessmethods/github/method_test.go b/api/ocm/extensions/accessmethods/github/method_test.go
new file mode 100644
index 000000000..aa1dc69fe
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/github/method_test.go
@@ -0,0 +1,280 @@
+package github_test
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+
+ _ "ocm.software/ocm/api/datacontext/config"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/filepath/pkg/filepath"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/github/identity"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/attrs/tmpcache"
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ me "ocm.software/ocm/api/ocm/extensions/accessmethods/github"
+)
+
+type mockDownloader struct {
+ expected []byte
+ err error
+}
+
+func (m *mockDownloader) Download(w io.WriterAt) error {
+ if _, err := w.WriteAt(m.expected, 0); err != nil {
+ return fmt.Errorf("failed to write to mock writer: %w", err)
+ }
+ return m.err
+}
+
+// RoundTripFunc .
+type RoundTripFunc func(req *http.Request) *http.Response
+
+// RoundTrip .
+func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
+ return f(req), nil
+}
+
+// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
+func NewTestClient(fn RoundTripFunc) *http.Client {
+ return &http.Client{
+ Transport: fn,
+ }
+}
+
+var _ = Describe("Method", func() {
+ var (
+ ctx ocm.Context
+ expectedBlobContent []byte
+ err error
+ defaultLink string
+ accessSpec *me.AccessSpec
+ fs vfs.FileSystem
+ expectedURL string
+ clientFn func(url string) *http.Client
+ )
+
+ BeforeEach(func() {
+ ctx = ocm.New()
+ expectedBlobContent, err = os.ReadFile(filepath.Join("testdata", "repo.tar.gz"))
+ Expect(err).ToNot(HaveOccurred())
+ defaultLink = "https://github.com/test/test/sha?token=token"
+ expectedURL = "https://api.github.com/repos/test/test/tarball/7b1445755ee2527f0bf80ef9eeb59a5d2e6e3e1f"
+
+ clientFn = func(url string) *http.Client {
+ return NewTestClient(func(req *http.Request) *http.Response {
+ if req.URL.String() != url {
+ Fail(fmt.Sprintf("failed to match url to expected url. want: %s; got: %s", expectedURL, req.URL.String()))
+ }
+ return &http.Response{
+ StatusCode: http.StatusFound,
+ Status: http.StatusText(http.StatusFound),
+ Body: io.NopCloser(bytes.NewBufferString(`{}`)),
+ Header: http.Header{
+ "Location": []string{defaultLink},
+ },
+ }
+ })
+ }
+
+ accessSpec = me.New(
+ "https://github.com/test/test",
+ "",
+ "7b1445755ee2527f0bf80ef9eeb59a5d2e6e3e1f",
+ me.WithClient(clientFn(expectedURL)),
+ me.WithDownloader(&mockDownloader{
+ expected: expectedBlobContent,
+ }),
+ )
+ fs, err = osfs.NewTempFileSystem()
+ Expect(err).To(Succeed())
+ vfsattr.Set(ctx, fs)
+ tmpcache.Set(ctx, &tmpcache.Attribute{Path: "/tmp", Filesystem: fs})
+ })
+
+ AfterEach(func() {
+ vfs.Cleanup(fs)
+ })
+
+ It("provides comsumer id", func() {
+ m, err := accessSpec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentials.GetProvidedConsumerId(m)).To(Equal(credentials.NewConsumerIdentity(identity.CONSUMER_TYPE,
+ identity.ID_HOSTNAME, "github.com",
+ identity.ID_PATHPREFIX, "test/test")))
+ })
+
+ It("downloads artifacts", func() {
+ m, err := accessSpec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).ToNot(HaveOccurred())
+ content, err := m.Get()
+ Expect(err).ToNot(HaveOccurred())
+ Expect(content).To(Equal(expectedBlobContent))
+ })
+
+ When("the commit sha is of an invalid length", func() {
+ It("errors", func() {
+ accessSpec := me.New(
+ "hostname",
+ "",
+ "not-a-sha",
+ me.WithClient(clientFn(expectedURL)),
+ me.WithDownloader(&mockDownloader{
+ expected: expectedBlobContent,
+ }),
+ )
+ m, err := accessSpec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).To(MatchError(ContainSubstring("commit is not a SHA")))
+ if m != nil {
+ m.Close()
+ }
+ })
+ })
+
+ When("the commit sha is of the right length but contains invalid characters", func() {
+ It("errors", func() {
+ accessSpec := me.New(
+ "hostname",
+ "1234",
+ "refs/heads/veryinteresting_branch_namess",
+ me.WithClient(clientFn(expectedURL)),
+ me.WithDownloader(&mockDownloader{
+ expected: expectedBlobContent,
+ }),
+ )
+ m, err := accessSpec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).To(MatchError(ContainSubstring("commit contains invalid characters for a SHA")))
+ if m != nil {
+ m.Close()
+ }
+ })
+ })
+
+ When("credentials are provided", func() {
+ BeforeEach(func() {
+ clientFn = func(url string) *http.Client {
+ return NewTestClient(func(req *http.Request) *http.Response {
+ if v, ok := req.Header["Authorization"]; ok {
+ Expect(v).To(ContainElement("Bearer test"))
+ } else {
+ Fail("Authorization header not found in request")
+ }
+ if req.URL.String() != url {
+ Fail(fmt.Sprintf("failed to match url to expected url. want: %s; got: %s", expectedURL, req.URL.String()))
+ }
+ return &http.Response{
+ StatusCode: http.StatusFound,
+ Status: http.StatusText(http.StatusFound),
+ // Must be set to non-nil value or it panics
+ Body: io.NopCloser(bytes.NewBufferString(`{}`)),
+ Header: http.Header{
+ "Location": []string{defaultLink},
+ },
+ }
+ })
+ }
+ accessSpec = me.New(
+ "https://github.com/test/test",
+ "",
+ "7b1445755ee2527f0bf80ef9eeb59a5d2e6e3e1f",
+ me.WithClient(clientFn(expectedURL)),
+ me.WithDownloader(&mockDownloader{
+ expected: expectedBlobContent,
+ }),
+ )
+ })
+ It("can use those to access private repos", func() {
+ mcc := ocm.New(datacontext.MODE_INITIAL)
+ src := &mockCredSource{
+ Context: mcc.CredentialsContext(),
+ cred: credentials.DirectCredentials{
+ credentials.ATTR_TOKEN: "test",
+ },
+ }
+ mcc.CredentialsContext().SetCredentialsForConsumer(credentials.NewConsumerIdentity(identity.CONSUMER_TYPE), src)
+ m, err := accessSpec.AccessMethod(&mockComponentVersionAccess{
+ ocmContext: mcc,
+ })
+ Expect(err).ToNot(HaveOccurred())
+ _, err = m.Get()
+ Expect(err).ToNot(HaveOccurred())
+ m.Close()
+ Expect(src.called).To(BeTrue())
+ })
+ })
+
+ When("GetCredentialsForConsumer returns an error", func() {
+ It("errors", func() {
+ mcc := ocm.New(datacontext.MODE_INITIAL)
+ src := &mockCredSource{
+ Context: mcc.CredentialsContext(),
+ err: fmt.Errorf("danger will robinson"),
+ }
+ mcc.CredentialsContext().SetCredentialsForConsumer(credentials.NewConsumerIdentity(identity.CONSUMER_TYPE), src)
+ _, err := accessSpec.AccessMethod(&mockComponentVersionAccess{
+ ocmContext: mcc,
+ })
+ Expect(err).To(MatchError(ContainSubstring("danger will robinson")))
+ Expect(src.called).To(BeTrue())
+ })
+ })
+
+ When("an enterprise repo URL is provided", func() {
+ It("uses that domain and includes api/v3 in the request URL", func() {
+ expectedURL = "https://github.tools.sap/api/v3/repos/test/test/tarball/25d9a3f0031c0b42e9ef7ab0117c35378040ef82"
+ spec := me.New("https://github.tools.sap/test/test", "", "25d9a3f0031c0b42e9ef7ab0117c35378040ef82", me.WithClient(clientFn(expectedURL)))
+ _, err := spec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+
+ When("hostname is different from github.com", func() {
+ It("will use an enterprise client", func() {
+ expectedURL = "https://custom/api/v3/repos/test/test/tarball/25d9a3f0031c0b42e9ef7ab0117c35378040ef82"
+ spec := me.New("https://github.tools.sap/test/test", "custom", "25d9a3f0031c0b42e9ef7ab0117c35378040ef82", me.WithClient(clientFn(expectedURL)))
+ _, err := spec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+
+ When("repoURL doesn't have an https prefix", func() {
+ It("will add one", func() {
+ expectedURL = "https://api.github.com/repos/test/test/tarball/25d9a3f0031c0b42e9ef7ab0117c35378040ef82"
+ spec := me.New("github.com/test/test", "", "25d9a3f0031c0b42e9ef7ab0117c35378040ef82", me.WithClient(clientFn(expectedURL)))
+ _, err := spec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: ctx})
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+})
+
+type mockComponentVersionAccess struct {
+ ocm.ComponentVersionAccess
+ ocmContext ocm.Context
+}
+
+func (m *mockComponentVersionAccess) GetContext() ocm.Context {
+ return m.ocmContext
+}
+
+type mockCredSource struct {
+ credentials.Context
+ cred credentials.Credentials
+ called bool
+ err error
+}
+
+func (m *mockCredSource) Credentials(credentials.Context, ...credentials.CredentialsSource) (credentials.Credentials, error) {
+ m.called = true
+ return m.cred, m.err
+}
diff --git a/pkg/contexts/ocm/accessmethods/github/suite_test.go b/api/ocm/extensions/accessmethods/github/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/github/suite_test.go
rename to api/ocm/extensions/accessmethods/github/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/github/testdata/repo.tar.gz b/api/ocm/extensions/accessmethods/github/testdata/repo.tar.gz
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/github/testdata/repo.tar.gz
rename to api/ocm/extensions/accessmethods/github/testdata/repo.tar.gz
diff --git a/pkg/contexts/ocm/accessmethods/helm/README.md b/api/ocm/extensions/accessmethods/helm/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/helm/README.md
rename to api/ocm/extensions/accessmethods/helm/README.md
diff --git a/api/ocm/extensions/accessmethods/helm/cli.go b/api/ocm/extensions/accessmethods/helm/cli.go
new file mode 100644
index 000000000..42f65a478
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/helm/cli.go
@@ -0,0 +1,53 @@
+package helm
+
+import (
+ "ocm.software/ocm/api/credentials/builtin/helm/identity"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.RepositoryOption,
+ options.PackageOption,
+ options.VersionOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.RepositoryOption, config, "helmRepository")
+ flagsets.AddFieldByOptionP(opts, options.PackageOption, config, "helmChart")
+ flagsets.AddFieldByOptionP(opts, options.VersionOption, config, "version")
+ return nil
+}
+
+var usage = `
+This method implements the access of a Helm chart stored in a Helm repository.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **helmRepository
** *string*
+
+ Helm repository URL.
+
+- **helmChart
** *string*
+
+ The name of the Helm chart and its version separated by a colon.
+
+- **version
** *string*
+
+ The version of the Helm chart if not specified as part of the chart name.
+
+- **caCert
** *string*
+
+ An optional TLS root certificate.
+
+- **keyring
** *string*
+
+ An optional keyring used to verify the chart.
+
+It uses the consumer identity type ` + identity.CONSUMER_TYPE + ` with the fields
+for a hostpath identity matcher (see localReference
** *string*
+
+ Repository type specific location information as string. The value
+ may encode any deep structure, but typically just an access path is sufficient.
+
+- **mediaType
** *string*
+
+ The media type of the blob used to store the resource. It may add
+ format information like +tar
or +gzip
.
+
+- **referenceName
** (optional) *string*
+
+ This optional attribute may contain identity information used by
+ other repositories to restore some global access with an identity
+ related to the original source.
+
+ For example, if an OCI artifact originally referenced using the
+ access method ociArtifact
is stored during
+ some transport step as local artifact, the reference name can be set
+ to its original repository name. An import step into an OCI based OCM
+ repository may then decide to make this artifact available again as
+ regular OCI artifact.
+
+- **globalAccess
** (optional) *access method specification*
+
+ If a resource blob is stored locally, the repository implementation
+ may decide to provide an external access information (independent
+ of the OCM model).
+
+ For example, an OCI artifact stored as local blob
+ can be additionally stored as regular OCI artifact in an OCI registry.
+
+ This additional external access information can be added using
+ a second external access method specification.
+`
diff --git a/api/ocm/extensions/accessmethods/localblob/method.go b/api/ocm/extensions/accessmethods/localblob/method.go
new file mode 100644
index 000000000..83fbcc4b0
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localblob/method.go
@@ -0,0 +1,188 @@
+package localblob
+
+import (
+ "encoding/json"
+ "fmt"
+
+ . "github.com/mandelsoft/goutils/exception"
+
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of a blob local to a component.
+const (
+ Type = "localBlob"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+// this package shows how to implement access types with multiple serialization versions.
+// So far, only one is implemented, but it shows how to add other ones.
+//
+// Specifications using multiple format versions allways provide a single common
+// *internal* Go representation, intended to be used by library users. Only this
+// internal version should be used outside this package. Additionally, there
+// are Go types representing the various format versions, which will be used
+// for the de-/serialization process (here AccessSpecV1).
+//
+// The supported versions are gathered in a dedicated scheme object (variable versions),
+// which is then used to register all available versions at the default scheme (see
+// init method).
+// The *internal* specification Go type (here AccessSpec) must be based on
+// runtime.InternalVersionedObjectType.
+// It is initialized with the effective type/version name and the versions scheme
+// and represents the Go representation used by API users, the format versions
+// are never used outside this package.
+//
+// Additionally, this *internal* type must implement the MarshalJSON method, which
+// can be implemented by delegating to the runtime.MarshalVersionedTypedObject
+// method, which evaluated the versions scheme to finds the applicable conversion
+// provided by the runtime.InternalVersionedObjectType.
+//
+// For every format version runtime.FormatVersion is required, which can be created
+// with cpi.NewAccessSpecVersion, which takes the prototype and a converter,
+// which converts between the internal go representation and the external formats,
+// given by a dedicated go Type with serialization annotations.
+
+var versions = accspeccpi.NewAccessTypeVersionScheme(Type)
+
+func init() {
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*AccessSpec, *AccessSpecV1](Type, &converterV1{}, accspeccpi.WithDescription(usage))))
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*AccessSpec, *AccessSpecV1](TypeV1, &converterV1{}, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler()))))
+ accspeccpi.RegisterAccessTypeVersions(versions)
+}
+
+func Is(spec accspeccpi.AccessSpec) bool {
+ return spec != nil && spec.GetKind() == Type
+}
+
+// New creates a new localFilesystemBlob accessor.
+func New(local, hint string, mediaType string, global accspeccpi.AccessSpec) *AccessSpec {
+ return &AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, Type),
+ LocalReference: local,
+ ReferenceName: hint,
+ MediaType: mediaType,
+ GlobalAccess: accspeccpi.NewAccessSpecRef(global),
+ }
+}
+
+func Decode(data []byte) (*AccessSpec, error) {
+ spec, err := versions.Decode(data, runtime.DefaultYAMLEncoding)
+ if err != nil {
+ return nil, err
+ }
+ return spec.(*AccessSpec), nil
+}
+
+// AccessSpec describes the access for a local blob.
+type AccessSpec struct {
+ runtime.InternalVersionedTypedObject[accspeccpi.AccessSpec]
+ // LocalReference is the repository local identity of the blob.
+ // it is used by the repository implementation to get access
+ // to the blob and if therefore specific to a dedicated repository type.
+ LocalReference string `json:"localReference"`
+ // MediaType is the media type of the object represented by the blob
+ MediaType string `json:"mediaType"`
+
+ // GlobalAccess is an optional field describing a possibility
+ // for a global access. If given, it MUST describe a global access method.
+ GlobalAccess *accspeccpi.AccessSpecRef `json:"globalAccess,omitempty"`
+ // ReferenceName is an optional static name the object should be
+ // use in a local repository context. It is use by a repository
+ // to optionally determine a globally referencable access according
+ // to the OCI distribution spec. The result will be stored
+ // by the repository in the field ImageReference.
+ // The value is typically an OCI repository name optionally
+ // followed by a colon ':' and a tag
+ ReferenceName string `json:"referenceName,omitempty"`
+}
+
+var (
+ _ json.Marshaler = (*AccessSpec)(nil)
+ _ accspeccpi.HintProvider = (*AccessSpec)(nil)
+ _ accspeccpi.GlobalAccessProvider = (*AccessSpec)(nil)
+ _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+)
+
+func (a AccessSpec) MarshalJSON() ([]byte, error) {
+ return runtime.MarshalVersionedTypedObject(&a)
+ // return cpi.MarshalConvertedAccessSpec(cpi.DefaultContext(), &a)
+}
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("Local blob %s[%s]", a.LocalReference, a.ReferenceName)
+}
+
+func (a *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return true
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ if g, err := ctx.AccessSpecForSpec(a.GlobalAccess); err == nil {
+ return g
+ }
+ return a.GlobalAccess.Unwrap()
+}
+
+func (a *AccessSpec) GetMimeType() string {
+ if a.MediaType == "" {
+ return mime.MIME_OCTET
+ }
+ return a.MediaType
+}
+
+func (a *AccessSpec) GetReferenceHint(cv accspeccpi.ComponentVersionAccess) string {
+ return a.ReferenceName
+}
+
+func (a *AccessSpec) AccessMethod(cv accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return cv.AccessMethod(a)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type AccessSpecV1 struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ // LocalReference is the repository local identity of the blob.
+ // it is used by the repository implementation to get access
+ // to the blob and if therefore specific to a dedicated repository type.
+ LocalReference string `json:"localReference"`
+ // MediaType is the media type of the object represented by the blob
+ MediaType string `json:"mediaType"`
+
+ // GlobalAccess is an optional field describing a possibility
+ // for a global access. If given, it MUST describe a global access method.
+ GlobalAccess *accspeccpi.AccessSpecRef `json:"globalAccess,omitempty"`
+ // ReferenceName is an optional static name the object should be
+ // use in a local repository context. It is use by a repository
+ // to optionally determine a globally referencable access according
+ // to the OCI distribution spec. The result will be stored
+ // by the repository in the field ImageReference.
+ // The value is typically an OCI repository name optionally
+ // followed by a colon ':' and a tag
+ ReferenceName string `json:"referenceName,omitempty"`
+}
+
+type converterV1 struct{}
+
+func (_ converterV1) ConvertFrom(in *AccessSpec) (*AccessSpecV1, error) {
+ return &AccessSpecV1{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(in.Type),
+ LocalReference: in.LocalReference,
+ ReferenceName: in.ReferenceName,
+ GlobalAccess: accspeccpi.NewAccessSpecRef(in.GlobalAccess),
+ MediaType: in.MediaType,
+ }, nil
+}
+
+func (_ converterV1) ConvertTo(in *AccessSpecV1) (*AccessSpec, error) {
+ return &AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, in.Type),
+ LocalReference: in.LocalReference,
+ ReferenceName: in.ReferenceName,
+ GlobalAccess: in.GlobalAccess,
+ MediaType: in.MediaType,
+ }, nil
+}
diff --git a/api/ocm/extensions/accessmethods/localblob/method_test.go b/api/ocm/extensions/accessmethods/localblob/method_test.go
new file mode 100644
index 000000000..097a518af
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localblob/method_test.go
@@ -0,0 +1,93 @@
+package localblob_test
+
+import (
+ "encoding/json"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociblob"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ CTF = "ctf"
+ COMPONENT = "fabianburth.org/component"
+ VERSION = "v1.0"
+ ARTIFACT_NAME = "artifact"
+ ARTIFACT_VERSION = "v1.0"
+)
+
+var _ = Describe("Method", func() {
+ data := `globalAccess:
+ digest: sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a
+ mediaType: application/tar+gzip
+ ref: ghcr.io/vasu1124/ocm/component-descriptors/github.com/vasu1124/introspect-delivery
+ size: 11287
+ type: ociBlob
+localReference: sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a
+mediaType: application/tar+gzip
+type: localBlob
+`
+ _ = data
+
+ It("marshal/unmarshal simple", func() {
+ spec := localblob.New("path", "hint", mime.MIME_TEXT, nil)
+ data := Must(json.Marshal(spec))
+ Expect(string(data)).To(Equal("{\"type\":\"localBlob\",\"localReference\":\"path\",\"mediaType\":\"text/plain\",\"referenceName\":\"hint\"}"))
+ r := Must(localblob.Decode(data))
+ Expect(r).To(Equal(spec))
+ })
+
+ It("marshal/unmarshal with global", func() {
+ spec := localblob.New("", "", "", nil)
+ Expect(runtime.DefaultYAMLEncoding.Unmarshal([]byte(data), spec)).To(Succeed())
+
+ r := Must(runtime.DefaultYAMLEncoding.Marshal(spec))
+ Expect(string(r)).To(Equal(data))
+
+ global := ociblob.New(
+ "ghcr.io/vasu1124/ocm/component-descriptors/github.com/vasu1124/introspect-delivery",
+ "sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a",
+ "application/tar+gzip",
+ 11287,
+ )
+ Expect(spec.GlobalAccess.Evaluate(ocm.DefaultContext())).To(Equal(global))
+
+ r = Must(runtime.DefaultYAMLEncoding.Marshal(spec))
+ Expect(string(r)).To(Equal(data))
+ })
+
+ It("check get inexpensive content version identity method", func() {
+ var env *Builder
+
+ env = NewBuilder()
+ defer env.Cleanup()
+
+ env.OCMCommonTransport(CTF, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMPONENT, VERSION, func() {
+ env.Resource(ARTIFACT_NAME, ARTIFACT_VERSION, resourcetypes.BLOB, metav1.LocalRelation, func() {
+ env.BlobData(mime.MIME_TEXT, []byte("testdata"))
+ })
+ })
+ })
+
+ repo := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, CTF, 0, env))
+ defer Close(repo)
+ cv := Must(repo.LookupComponentVersion(COMPONENT, VERSION))
+ defer Close(cv)
+ access := cv.GetDescriptor().Resources[0].Access
+ spec := Must(env.OCMContext().AccessSpecForSpec(access))
+ Expect(spec.GetVersion()).To(Equal("v1"))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/localblob/suite_test.go b/api/ocm/extensions/accessmethods/localblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/localblob/suite_test.go
rename to api/ocm/extensions/accessmethods/localblob/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/localfsblob/method.go b/api/ocm/extensions/accessmethods/localfsblob/method.go
new file mode 100644
index 000000000..50c9895d6
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localfsblob/method.go
@@ -0,0 +1,76 @@
+package localfsblob
+
+import (
+ . "github.com/mandelsoft/goutils/exception"
+
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of a blob in a local filesystem.
+const (
+ Type = "localFilesystemBlob"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+// Keep old access method and map generic one to this implementation for component archives
+
+// This method uses the localblob internal format and converts it to/from the
+// appropriate serialization version.
+// The attributes referenceName and globalAccess are NOT supported.
+
+var versions = accspeccpi.NewAccessTypeVersionScheme(Type)
+
+func init() {
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*localblob.AccessSpec, *AccessSpec](Type, &converterV1{})))
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*localblob.AccessSpec, *AccessSpec](TypeV1, &converterV1{})))
+ accspeccpi.RegisterAccessTypeVersions(versions)
+}
+
+// New creates a new localFilesystemBlob accessor.
+func New(path string, media string) *localblob.AccessSpec {
+ return &localblob.AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, Type),
+ LocalReference: path,
+ MediaType: media,
+ }
+}
+
+func Decode(data []byte) (*localblob.AccessSpec, error) {
+ spec, err := versions.Decode(data, runtime.DefaultYAMLEncoding)
+ if err != nil {
+ return nil, err
+ }
+ return spec.(*localblob.AccessSpec), nil
+}
+
+// AccessSpec describes the access for a blob on the filesystem.
+// Deprecated: use LocalBlob.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ // FileName is the
+ Filename string `json:"fileName"`
+ // MediaType is the media type of the object represented by the blob
+ MediaType string `json:"mediaType"`
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type converterV1 struct{}
+
+func (_ converterV1) ConvertFrom(in *localblob.AccessSpec) (*AccessSpec, error) {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(in.Type),
+ Filename: in.LocalReference,
+ MediaType: in.MediaType,
+ }, nil
+}
+
+func (_ converterV1) ConvertTo(in *AccessSpec) (*localblob.AccessSpec, error) {
+ return &localblob.AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, in.Type),
+ LocalReference: in.Filename,
+ MediaType: in.MediaType,
+ }, nil
+}
diff --git a/api/ocm/extensions/accessmethods/localfsblob/method_test.go b/api/ocm/extensions/accessmethods/localfsblob/method_test.go
new file mode 100644
index 000000000..d5bc28601
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localfsblob/method_test.go
@@ -0,0 +1,34 @@
+package localfsblob_test
+
+import (
+ "encoding/json"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localfsblob"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+var _ = Describe("Method", func() {
+ data := `globalAccess:
+ digest: sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a
+ mediaType: application/tar+gzip
+ ref: ghcr.io/vasu1124/ocm/component-descriptors/github.com/vasu1124/introspect-delivery
+ size: 11287
+ type: ociBlob
+localReference: sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a
+mediaType: application/tar+gzip
+type: localBlob
+`
+ _ = data
+
+ It("marshal/unmarshal simple", func() {
+ spec := localfsblob.New("path", mime.MIME_TEXT)
+ data := Must(json.Marshal(spec))
+ Expect(string(data)).To(Equal("{\"type\":\"localFilesystemBlob\",\"fileName\":\"path\",\"mediaType\":\"text/plain\"}"))
+ r := Must(localfsblob.Decode(data))
+ Expect(r).To(Equal(spec))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/localfsblob/suite_test.go b/api/ocm/extensions/accessmethods/localfsblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/localfsblob/suite_test.go
rename to api/ocm/extensions/accessmethods/localfsblob/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/localociblob/method.go b/api/ocm/extensions/accessmethods/localociblob/method.go
new file mode 100644
index 000000000..034f34a70
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localociblob/method.go
@@ -0,0 +1,70 @@
+package localociblob
+
+import (
+ . "github.com/mandelsoft/goutils/exception"
+
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type for a component version local blob in an OCI repository.
+const (
+ Type = "localOciBlob"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+var versions = accspeccpi.NewAccessTypeVersionScheme(Type)
+
+func init() {
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*localblob.AccessSpec, *AccessSpec](Type, &converterV1{})))
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByConverter[*localblob.AccessSpec, *AccessSpec](TypeV1, &converterV1{})))
+ accspeccpi.RegisterAccessTypeVersions(versions)
+}
+
+// New creates a new LocalOCIBlob accessor.
+// Deprecated: Use LocalBlob.
+func New(digest digest.Digest) *localblob.AccessSpec {
+ return &localblob.AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, Type),
+ LocalReference: digest.String(),
+ }
+}
+
+func Decode(data []byte) (*localblob.AccessSpec, error) {
+ spec, err := versions.Decode(data, runtime.DefaultYAMLEncoding)
+ if err != nil {
+ return nil, err
+ }
+ return spec.(*localblob.AccessSpec), nil
+}
+
+// AccessSpec describes the access for a oci registry.
+// Deprecated: Use LocalBlob.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // Digest is the digest of the targeted content.
+ Digest digest.Digest `json:"digest"`
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type converterV1 struct{}
+
+func (_ converterV1) ConvertFrom(in *localblob.AccessSpec) (*AccessSpec, error) {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(in.Type),
+ Digest: digest.Digest(in.LocalReference),
+ }, nil
+}
+
+func (_ converterV1) ConvertTo(in *AccessSpec) (*localblob.AccessSpec, error) {
+ return &localblob.AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, in.Type),
+ LocalReference: in.Digest.String(),
+ MediaType: "",
+ }, nil
+}
diff --git a/api/ocm/extensions/accessmethods/localociblob/method_test.go b/api/ocm/extensions/accessmethods/localociblob/method_test.go
new file mode 100644
index 000000000..e0d324930
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/localociblob/method_test.go
@@ -0,0 +1,21 @@
+package localociblob_test
+
+import (
+ "encoding/json"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localociblob"
+)
+
+var _ = Describe("Method", func() {
+ It("marshal/unmarshal simple", func() {
+ spec := localociblob.New("sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a")
+ data := Must(json.Marshal(spec))
+ Expect(string(data)).To(Equal("{\"type\":\"localOciBlob\",\"digest\":\"sha256:1bf729fa00e355199e711933ccfa27467ee3d2de1343aef2a7c1ecbdf885e63a\"}"))
+ r := Must(localociblob.Decode(data))
+ Expect(r).To(Equal(spec))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/localociblob/suite_test.go b/api/ocm/extensions/accessmethods/localociblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/localociblob/suite_test.go
rename to api/ocm/extensions/accessmethods/localociblob/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/maven/README.md b/api/ocm/extensions/accessmethods/maven/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/maven/README.md
rename to api/ocm/extensions/accessmethods/maven/README.md
diff --git a/api/ocm/extensions/accessmethods/maven/cli.go b/api/ocm/extensions/accessmethods/maven/cli.go
new file mode 100644
index 000000000..4af69b8ef
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/maven/cli.go
@@ -0,0 +1,62 @@
+package maven
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.RepositoryOption,
+ options.GroupOption,
+ options.ArtifactOption,
+ options.VersionOption,
+ // optional
+ options.ClassifierOption,
+ options.ExtensionOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.RepositoryOption, config, "repoUrl")
+ flagsets.AddFieldByOptionP(opts, options.GroupOption, config, "groupId")
+ flagsets.AddFieldByOptionP(opts, options.PackageOption, config, "artifactId")
+ flagsets.AddFieldByOptionP(opts, options.VersionOption, config, "version")
+ // optional
+ flagsets.AddFieldByOptionP(opts, options.ClassifierOption, config, "classifier")
+ flagsets.AddFieldByOptionP(opts, options.ExtensionOption, config, "extension")
+ return nil
+}
+
+var usage = `
+This method implements the access of a Maven artifact in a Maven repository.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **repoUrl
** *string*
+
+ URL of the Maven repository
+
+- **groupId
** *string*
+
+ The groupId of the Maven artifact
+
+- **artifactId
** *string*
+
+ The artifactId of the Maven artifact
+
+- **version
** *string*
+
+ The version name of the Maven artifact
+
+- **classifier
** *string*
+
+ The optional classifier of the Maven artifact
+
+- **extension
** *string*
+
+ The optional extension of the Maven artifact
+`
diff --git a/api/ocm/extensions/accessmethods/maven/method.go b/api/ocm/extensions/accessmethods/maven/method.go
new file mode 100644
index 000000000..e37cad5bc
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/maven/method.go
@@ -0,0 +1,132 @@
+package maven
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/tech/maven"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ mavenblob "ocm.software/ocm/api/utils/blobaccess/maven"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of Maven repository.
+const (
+ Type = "maven"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+}
+
+// AccessSpec describes the access for a Maven artifact.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // RepoUrl is the base URL of the Maven repository.
+ RepoUrl string `json:"repoUrl"`
+
+ maven.Coordinates `json:",inline"`
+}
+
+// Option defines the interface function "ApplyTo()".
+type Option = maven.CoordinateOption
+
+type WithClassifier = maven.WithClassifier
+
+func WithOptionalClassifier(c *string) Option {
+ return maven.WithOptionalClassifier(c)
+}
+
+type WithExtension = maven.WithExtension
+
+func WithOptionalExtension(e *string) Option {
+ return maven.WithOptionalExtension(e)
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+// New creates a new Maven repository access spec version v1.
+func New(repository, groupId, artifactId, version string, opts ...Option) *AccessSpec {
+ accessSpec := &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ RepoUrl: repository,
+ Coordinates: *maven.NewCoordinates(groupId, artifactId, version, opts...),
+ }
+ return accessSpec
+}
+
+// NewForCoordinates creates a new Maven repository access spec version v1.
+func NewForCoordinates(repository string, coords *maven.Coordinates, opts ...Option) *AccessSpec {
+ optionutils.ApplyOptions(coords, opts...)
+ accessSpec := &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ RepoUrl: repository,
+ Coordinates: *coords,
+ }
+ return accessSpec
+}
+
+func (a *AccessSpec) Describe(_ accspeccpi.Context) string {
+ return fmt.Sprintf("Maven package '%s' in repository '%s' path '%s'", a.Coordinates.String(), a.RepoUrl, a.Coordinates.FilePath())
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) GlobalAccessSpec(_ accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+// GetReferenceHint returns the reference hint for the Maven (mvn) artifact.
+func (a *AccessSpec) GetReferenceHint(_ accspeccpi.ComponentVersionAccess) string {
+ if a.IsPackage() {
+ return a.GAV()
+ }
+ return ""
+}
+
+func (_ *AccessSpec) GetType() string {
+ return Type
+}
+
+func (a *AccessSpec) AccessMethod(cv accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ octx := cv.GetContext()
+
+ repo, err := maven.NewUrlRepository(a.RepoUrl, vfsattr.Get(cv.GetContext()))
+ if err != nil {
+ return nil, err
+ }
+
+ factory := func() (blobaccess.BlobAccess, error) {
+ return mavenblob.BlobAccessForCoords(repo, &a.Coordinates,
+ mavenblob.WithCredentialContext(octx),
+ mavenblob.WithLoggingContext(octx),
+ mavenblob.WithCachingFileSystem(vfsattr.Get(octx)))
+ }
+ return accspeccpi.AccessMethodForImplementation(accspeccpi.NewDefaultMethodImpl(cv, a, "", a.MimeType(), factory), nil)
+}
+
+func (a *AccessSpec) BaseUrl() string {
+ return a.RepoUrl + "/" + a.GavPath()
+}
+
+func (a *AccessSpec) ArtifactUrl() string {
+ repo, err := maven.NewUrlRepository(a.RepoUrl)
+ if err != nil {
+ return ""
+ }
+ return a.Location(repo).String()
+}
+
+func (a *AccessSpec) GetCoordinates() *maven.Coordinates {
+ return a.Coordinates.Copy()
+}
diff --git a/api/ocm/extensions/accessmethods/maven/method_test.go b/api/ocm/extensions/accessmethods/maven/method_test.go
new file mode 100644
index 000000000..5874b5548
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/maven/method_test.go
@@ -0,0 +1,137 @@
+package maven_test
+
+import (
+ "crypto"
+ "time"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ me "ocm.software/ocm/api/ocm/extensions/accessmethods/maven"
+ "ocm.software/ocm/api/tech/maven/maventest"
+ "ocm.software/ocm/api/utils/iotools"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/tarutils"
+)
+
+const (
+ MAVEN_PATH = "/testdata/.m2/repository"
+ FAILPATH = "/testdata/.m2/fail"
+ MAVEN_CENTRAL = "https://repo.maven.apache.org/maven2/"
+ MAVEN_CENTRAL_ADDRESS = "repo.maven.apache.org:443"
+ MAVEN_GROUP_ID = "maven"
+ MAVEN_ARTIFACT_ID = "maven"
+ MAVEN_VERSION = "1.1"
+)
+
+var _ = Describe("local accessmethods.maven.AccessSpec tests", func() {
+ var env *Builder
+ var cv ocm.ComponentVersionAccess
+
+ BeforeEach(func() {
+ env = NewBuilder(maventest.TestData())
+ cv = &cpi.DummyComponentVersionAccess{env.OCMContext()}
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("accesses local artifact", func() {
+ acc := me.New("file://"+MAVEN_PATH, "com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0")
+ m := Must(acc.AccessMethod(cv))
+ defer Close(m)
+ Expect(m.MimeType()).To(Equal(mime.MIME_TGZ))
+ r := Must(m.Reader())
+ defer Close(r)
+ dr := iotools.NewDigestReaderWithHash(crypto.SHA256, r)
+ li := Must(tarutils.ListArchiveContentFromReader(dr))
+ Expect(li).To(ConsistOf(
+ "sdk-modules-bom-5.7.0-random-content.json",
+ "sdk-modules-bom-5.7.0-random-content.txt",
+ "sdk-modules-bom-5.7.0-sources.jar",
+ "sdk-modules-bom-5.7.0.jar",
+ "sdk-modules-bom-5.7.0.pom"))
+ Expect(dr.Size()).To(Equal(int64(maventest.ARTIFACT_SIZE)))
+ Expect(dr.Digest().String()).To(Equal("SHA-256:" + maventest.ARTIFACT_DIGEST))
+ })
+ It("test empty repoUrl", func() {
+ acc := me.New("", "com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0")
+ ExpectError(acc.AccessMethod(cv)).ToNot(BeNil())
+ })
+
+ It("accesses local artifact with empty classifier and with extension", func() {
+ acc := me.New("file://"+MAVEN_PATH, "com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0", me.WithClassifier(""), me.WithExtension("pom"))
+ m := Must(acc.AccessMethod(cv))
+ defer Close(m)
+ Expect(m.MimeType()).To(Equal(mime.MIME_XML))
+ r := Must(m.Reader())
+ defer Close(r)
+
+ dr := iotools.NewDigestReaderWithHash(crypto.SHA1, r)
+ for {
+ var buf [8096]byte
+ _, err := dr.Read(buf[:])
+ if err != nil {
+ break
+ }
+ }
+
+ Expect(dr.Size()).To(Equal(int64(7153)))
+ Expect(dr.Digest().String()).To(Equal(maventest.POM_SHA1))
+ })
+
+ It("accesses local artifact with extension", func() {
+ acc := me.New("file://"+MAVEN_PATH, "com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0", me.WithExtension("pom"))
+ m := Must(acc.AccessMethod(cv))
+ defer Close(m)
+ Expect(m.MimeType()).To(Equal(mime.MIME_TGZ))
+ r := Must(m.Reader())
+ defer Close(r)
+ dr := iotools.NewDigestReaderWithHash(crypto.SHA1, r)
+ list := Must(tarutils.ListArchiveContentFromReader(dr))
+ Expect(list).To(ConsistOf("sdk-modules-bom-5.7.0.pom"))
+
+ Expect(dr.Size()).To(Equal(int64(1109)))
+ Expect(dr.Digest().String()).To(Equal("SHA-1:4ee125ffe4f7690588833f1217a13cc741e4df5f"))
+ })
+
+ It("Describe", func() {
+ acc := me.New("file://"+FAILPATH, "test", "repository", "42", me.WithExtension("pom"))
+ Expect(acc.Describe(nil)).To(Equal("Maven package 'test:repository:42::pom' in repository 'file:///testdata/.m2/fail' path 'test/repository/42/repository-42.pom'"))
+ })
+
+ It("detects digests mismatch", func() {
+ acc := me.New("file://"+FAILPATH, "test", "repository", "42", me.WithExtension("pom"))
+ m := Must(acc.AccessMethod(cv))
+ defer Close(m)
+ _, err := m.Reader()
+ Expect(err).To(MatchError(ContainSubstring("SHA-1 digest mismatch: expected 44a77645201d1a8fc5213ace787c220eabbd0967, found b3242b8c31f8ce14f729b8fd132ac77bc4bc5bf7")))
+ })
+
+ Context("me http repository", func() {
+ if PingTCPServer(MAVEN_CENTRAL_ADDRESS, time.Second) == nil {
+ It("blobaccess for gav", func() {
+ acc := me.New(MAVEN_CENTRAL, MAVEN_GROUP_ID, MAVEN_ARTIFACT_ID, MAVEN_VERSION)
+ m := Must(acc.AccessMethod(cv))
+ defer Close(m)
+ files := Must(tarutils.ListArchiveContentFromReader(Must(m.Reader())))
+ Expect(files).To(ConsistOf(
+ "maven-1.1-RC1.javadoc.javadoc.jar",
+ "maven-1.1-sources.jar",
+ "maven-1.1.jar",
+ "maven-1.1.pom",
+ ))
+ })
+
+ It("inexpensive id", func() {
+ acc := me.New(MAVEN_CENTRAL, MAVEN_GROUP_ID, MAVEN_ARTIFACT_ID, MAVEN_VERSION, me.WithClassifier(""), me.WithExtension("pom"))
+ Expect(acc.ArtifactId).To(Equal("maven"))
+ })
+ }
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/maven/suite_test.go b/api/ocm/extensions/accessmethods/maven/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/maven/suite_test.go
rename to api/ocm/extensions/accessmethods/maven/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/none/method.go b/api/ocm/extensions/accessmethods/none/method.go
new file mode 100644
index 000000000..7804cd878
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/none/method.go
@@ -0,0 +1,96 @@
+package none
+
+import (
+ "io"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm/compdesc"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type for no blob.
+const (
+ Type = compdesc.NoneType
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+ LegacyType = compdesc.NoneLegacyType
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription("dummy resource with no access")))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType))
+}
+
+// New creates a new OCIBlob accessor.
+func New() *AccessSpec {
+ return &AccessSpec{ObjectVersionedType: runtime.NewVersionedTypedObject(Type)}
+}
+
+func IsNone(kind string) bool {
+ return compdesc.IsNoneAccessKind(kind)
+}
+
+// AccessSpec describes the access for a oci registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return "none"
+}
+
+func (s *AccessSpec) IsLocal(context accspeccpi.Context) bool {
+ return false
+}
+
+func (s *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return nil
+}
+
+func (s *AccessSpec) GetMimeType() string {
+ return ""
+}
+
+func (s *AccessSpec) AccessMethod(access accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(&accessMethod{spec: s}, nil)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type accessMethod struct {
+ spec *AccessSpec
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Close() error {
+ return nil
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ return nil, errors.ErrNotSupported("access")
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ return nil, errors.ErrNotSupported("access")
+}
+
+func (m *accessMethod) MimeType() string {
+ return ""
+}
diff --git a/pkg/contexts/ocm/accessmethods/npm/README.md b/api/ocm/extensions/accessmethods/npm/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/npm/README.md
rename to api/ocm/extensions/accessmethods/npm/README.md
diff --git a/api/ocm/extensions/accessmethods/npm/cli.go b/api/ocm/extensions/accessmethods/npm/cli.go
new file mode 100644
index 000000000..d2299149c
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/npm/cli.go
@@ -0,0 +1,42 @@
+package npm
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.RegistryOption,
+ options.PackageOption,
+ options.VersionOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.RegistryOption, config, "registry")
+ flagsets.AddFieldByOptionP(opts, options.PackageOption, config, "package")
+ flagsets.AddFieldByOptionP(opts, options.VersionOption, config, "version")
+ return nil
+}
+
+var usage = `
+This method implements the access of an NPM package in an NPM registry.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **registry
** *string*
+
+ Base URL of the NPM registry.
+
+- **package
** *string*
+
+ The name of the NPM package
+
+- **version
** *string*
+
+ The version name of the NPM package
+`
diff --git a/api/ocm/extensions/accessmethods/npm/method.go b/api/ocm/extensions/accessmethods/npm/method.go
new file mode 100644
index 000000000..8131fb851
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/npm/method.go
@@ -0,0 +1,228 @@
+package npm
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/tech/npm"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/iotools"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of NPM registry.
+const (
+ Type = "npm"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+}
+
+// AccessSpec describes the access for a NPM registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // Registry is the base URL of the NPM registry
+ Registry string `json:"registry"`
+ // Package is the name of NPM package
+ Package string `json:"package"`
+ // Version of the NPM package.
+ Version string `json:"version"`
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+// New creates a new NPM registry access spec version v1.
+func New(registry, pkg, version string) *AccessSpec {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ Registry: registry,
+ Package: pkg,
+ Version: version,
+ }
+}
+
+func (a *AccessSpec) Describe(_ accspeccpi.Context) string {
+ return fmt.Sprintf("NPM package %s:%s in registry %s", a.Package, a.Version, a.Registry)
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) GlobalAccessSpec(_ accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+func (a *AccessSpec) GetReferenceHint(_ accspeccpi.ComponentVersionAccess) string {
+ return a.Package + ":" + a.Version
+}
+
+func (_ *AccessSpec) GetType() string {
+ return Type
+}
+
+func (a *AccessSpec) AccessMethod(c accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(newMethod(c, a))
+}
+
+// PackageUrl returns the URL of the NPM package (Registry/Package).
+func (a *AccessSpec) PackageUrl() string {
+ return strings.TrimSuffix(a.Registry, "/") + path.Join("/", a.Package)
+}
+
+// PackageVersionUrl returns the URL of the NPM package-version (Registry/Package/Version).
+func (a *AccessSpec) PackageVersionUrl() string {
+ return strings.TrimSuffix(a.Registry, "/") + path.Join("/", a.Package, a.Version)
+}
+
+func (a *AccessSpec) GetPackageVersion(ctx accspeccpi.Context) (*npm.Version, error) {
+ r, err := reader(a, vfsattr.Get(ctx), ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ buf, err := io.ReadAll(r)
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot get version metadata for %s", a.PackageVersionUrl())
+ }
+ var version npm.Version
+ err = json.Unmarshal(buf, &version)
+ if err != nil || version.Dist.Tarball == "" {
+ // ugly fallback as workaround for https://github.com/sonatype/nexus-public/issues/224
+ var project npm.Project
+ err = json.Unmarshal(buf, &project) // parse the complete project
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot unmarshal version metadata for %s", a.PackageVersionUrl())
+ }
+ v, ok := project.Version[a.Version] // and pick only the specified version
+ if !ok {
+ return nil, errors.Newf("version '%s' doesn't exist", a.Version)
+ }
+ version = v
+ }
+ return &version, nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (accspeccpi.AccessMethodImpl, error) {
+ factory := func() (blobaccess.BlobAccess, error) {
+ meta, err := a.GetPackageVersion(c.GetContext())
+ if err != nil {
+ return nil, err
+ }
+
+ f := func() (io.ReadCloser, error) {
+ return reader(a, vfsattr.Get(c.GetContext()), c.GetContext(), meta.Dist.Tarball)
+ }
+ if meta.Dist.Integrity != "" {
+ tf := f
+ f = func() (io.ReadCloser, error) {
+ r, err := tf()
+ if err != nil {
+ return nil, err
+ }
+ digest, err := iotools.DecodeBase64ToHex(meta.Dist.Integrity)
+ if err != nil {
+ return nil, err
+ }
+ return iotools.VerifyingReaderWithHash(r, crypto.SHA512, digest), nil
+ }
+ }
+ if meta.Dist.Shasum != "" {
+ tf := f
+ f = func() (io.ReadCloser, error) {
+ r, err := tf()
+ if err != nil {
+ return nil, err
+ }
+ return iotools.VerifyingReaderWithHash(r, crypto.SHA1, meta.Dist.Shasum), nil
+ }
+ }
+ acc := blobaccess.DataAccessForReaderFunction(f, meta.Dist.Tarball)
+ return accessobj.CachedBlobAccessForWriter(c.GetContext(), mime.MIME_TGZ, accessio.NewDataAccessWriter(acc)), nil
+ }
+ return accspeccpi.NewDefaultMethodImpl(c, a, "", mime.MIME_TGZ, factory), nil
+}
+
+func reader(a *AccessSpec, fs vfs.FileSystem, ctx cpi.ContextProvider, tar ...string) (io.ReadCloser, error) {
+ url := a.PackageVersionUrl()
+ if len(tar) > 0 {
+ url = tar[0]
+ }
+ if strings.HasPrefix(url, "file://") {
+ path := url[7:]
+ return fs.OpenFile(path, vfs.O_RDONLY, 0o600)
+ }
+
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ err = npm.BasicAuth(req, ctx, a.Registry, a.Package)
+ if err != nil {
+ return nil, err
+ }
+ c := &http.Client{}
+ resp, err := c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNotFound {
+ // maybe it's stupid Nexus - https://github.com/sonatype/nexus-public/issues/224?
+ url = a.PackageUrl()
+ req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ err = npm.BasicAuth(req, ctx, a.Registry, a.Package)
+ if err != nil {
+ return nil, err
+ }
+
+ // close body before overwriting to close any pending connections
+ resp.Body.Close()
+ resp, err = c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ defer resp.Body.Close()
+ buf := &bytes.Buffer{}
+ _, err = io.Copy(buf, io.LimitReader(resp.Body, 2000))
+ if err != nil {
+ return nil, errors.Newf("version meta data request %s provides %s", url, resp.Status)
+ }
+ return nil, errors.Newf("version meta data request %s provides %s: %s", url, resp.Status, buf.String())
+ }
+ content, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ return io.NopCloser(bytes.NewBuffer(content)), nil
+}
diff --git a/api/ocm/extensions/accessmethods/npm/method_test.go b/api/ocm/extensions/accessmethods/npm/method_test.go
new file mode 100644
index 000000000..5bd3484f8
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/npm/method_test.go
@@ -0,0 +1,83 @@
+package npm_test
+
+import (
+ "crypto"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/helper/env"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/npm"
+ "ocm.software/ocm/api/utils/iotools"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const (
+ NPMPATH = "/testdata/registry"
+ FAILPATH = "/testdata/failregistry"
+)
+
+var _ = Describe("Method", func() {
+ var env *Builder
+ var cv ocm.ComponentVersionAccess
+
+ BeforeEach(func() {
+ env = NewBuilder(TestData())
+ cv = &cpi.DummyComponentVersionAccess{env.OCMContext()}
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("accesses artifact", func() {
+ acc := npm.New("file://"+NPMPATH, "yargs", "17.7.1")
+ // acc := npm.New("https://registry.npmjs.org", "yargs", "17.7.1")
+
+ m := Must(acc.AccessMethod(cv))
+ defer m.Close()
+ Expect(m.MimeType()).To(Equal(mime.MIME_TGZ))
+
+ r := Must(m.Reader())
+ defer r.Close()
+ dr := iotools.NewDigestReaderWithHash(crypto.SHA1, r)
+ for {
+ var buf [8096]byte
+ _, err := dr.Read(buf[:])
+ if err != nil {
+ break
+ }
+ }
+ Expect(dr.Size()).To(Equal(int64(65690)))
+ Expect(dr.Digest().String()).To(Equal("SHA-1:34a77645201d1a8fc5213ace787c220eabbd0967"))
+ })
+
+ It("detects digests mismatch", func() {
+ acc := npm.New("file://"+FAILPATH, "yargs", "17.7.1")
+
+ m := Must(acc.AccessMethod(cv))
+ defer m.Close()
+ _, err := m.Reader()
+ Expect(err).To(MatchError(ContainSubstring("SHA-1 digest mismatch: expected 44a77645201d1a8fc5213ace787c220eabbd0967, found 34a77645201d1a8fc5213ace787c220eabbd0967")))
+ })
+
+ It("PackageUrl()", func() {
+ packageUrl := "https://registry.npmjs.org/yargs"
+ acc := npm.New("https://registry.npmjs.org", "yargs", "17.7.1")
+ Expect(acc.PackageUrl()).To(Equal(packageUrl))
+ acc = npm.New("https://registry.npmjs.org/", "yargs", "17.7.1")
+ Expect(acc.PackageUrl()).To(Equal(packageUrl))
+ })
+
+ It("PackageVersionUrl()", func() {
+ packageVersionUrl := "https://registry.npmjs.org/yargs/17.7.1"
+ acc := npm.New("https://registry.npmjs.org", "yargs", "17.7.1")
+ Expect(acc.PackageVersionUrl()).To(Equal(packageVersionUrl))
+ acc = npm.New("https://registry.npmjs.org/", "yargs", "17.7.1")
+ Expect(acc.PackageVersionUrl()).To(Equal(packageVersionUrl))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/npm/suite_test.go b/api/ocm/extensions/accessmethods/npm/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/npm/suite_test.go
rename to api/ocm/extensions/accessmethods/npm/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/npm/testdata/content/yargs/yargs-17.7.1.tgz b/api/ocm/extensions/accessmethods/npm/testdata/content/yargs/yargs-17.7.1.tgz
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/npm/testdata/content/yargs/yargs-17.7.1.tgz
rename to api/ocm/extensions/accessmethods/npm/testdata/content/yargs/yargs-17.7.1.tgz
diff --git a/pkg/contexts/ocm/accessmethods/npm/testdata/failregistry/yargs/17.7.1 b/api/ocm/extensions/accessmethods/npm/testdata/failregistry/yargs/17.7.1
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/npm/testdata/failregistry/yargs/17.7.1
rename to api/ocm/extensions/accessmethods/npm/testdata/failregistry/yargs/17.7.1
diff --git a/pkg/contexts/ocm/accessmethods/npm/testdata/registry/yargs/17.7.1 b/api/ocm/extensions/accessmethods/npm/testdata/registry/yargs/17.7.1
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/npm/testdata/registry/yargs/17.7.1
rename to api/ocm/extensions/accessmethods/npm/testdata/registry/yargs/17.7.1
diff --git a/pkg/contexts/ocm/accessmethods/ociartifact/README.md b/api/ocm/extensions/accessmethods/ociartifact/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/ociartifact/README.md
rename to api/ocm/extensions/accessmethods/ociartifact/README.md
diff --git a/api/ocm/extensions/accessmethods/ociartifact/cli.go b/api/ocm/extensions/accessmethods/ociartifact/cli.go
new file mode 100644
index 000000000..bad0ab8d1
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociartifact/cli.go
@@ -0,0 +1,32 @@
+package ociartifact
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.ReferenceOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.ReferenceOption, config, "imageReference")
+ return nil
+}
+
+var usage = `
+This method implements the access of an OCI artifact stored in an OCI registry.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **imageReference
** *string*
+
+ OCI image/artifact reference following the possible docker schemes:
+ - <repo>/<artifact>:<digest>@<tag>
+ - [<port>]/<repo path>/<artifact>:<version>@<tag>
+`
diff --git a/api/ocm/extensions/accessmethods/ociartifact/logging.go b/api/ocm/extensions/accessmethods/ociartifact/logging.go
new file mode 100644
index 000000000..8f2e5b19e
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociartifact/logging.go
@@ -0,0 +1,30 @@
+package ociartifact
+
+import (
+ "github.com/mandelsoft/logging"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var REALM = ocmlog.DefineSubRealm("access method ociArtifact", "accessmethod/ociartifact")
+
+type ContextProvider interface {
+ GetContext() cpi.Context
+}
+
+func Logger(c ContextProvider, keyValuePairs ...interface{}) logging.Logger {
+ return c.GetContext().Logger(REALM).WithValues(keyValuePairs...)
+}
+
+type localContextProvider struct {
+ cpi.ContextProvider
+}
+
+func (l *localContextProvider) GetContext() cpi.Context {
+ return l.OCMContext()
+}
+
+func WrapContextProvider(ctx cpi.ContextProvider) ContextProvider {
+ return &localContextProvider{ctx}
+}
diff --git a/api/ocm/extensions/accessmethods/ociartifact/method.go b/api/ocm/extensions/accessmethods/ociartifact/method.go
new file mode 100644
index 000000000..2e6e616e5
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociartifact/method.go
@@ -0,0 +1,362 @@
+package ociartifact
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+
+ . "github.com/mandelsoft/goutils/finalizer"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/credentials"
+ ociidentity "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/ocireg"
+ "ocm.software/ocm/api/oci/grammar"
+ ocmcpi "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/logging"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of a oci registry.
+const (
+ Type = "ociArtifact"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+const (
+ LegacyType = "ociRegistry"
+ LegacyTypeV1 = LegacyType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyType))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](LegacyTypeV1))
+}
+
+func Is(spec accspeccpi.AccessSpec) bool {
+ return spec != nil && (spec.GetKind() == Type || spec.GetKind() == LegacyType)
+}
+
+// AccessSpec describes the access for a oci registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // ImageReference is the actual reference to the oci image repository and tag.
+ ImageReference string `json:"imageReference"`
+}
+
+var (
+ _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+ _ accspeccpi.HintProvider = (*AccessSpec)(nil)
+ _ blobaccess.DigestSource = (*AccessSpec)(nil)
+)
+
+// New creates a new oci registry access spec version v1.
+func New(ref string) *AccessSpec {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ ImageReference: ref,
+ }
+}
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("OCI artifact %s", a.ImageReference)
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) Digest() digest.Digest {
+ ref, err := oci.ParseRef(a.ImageReference)
+ if err != nil || ref.Digest == nil {
+ return ""
+ }
+ return *ref.Digest
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+func (a *AccessSpec) GetReferenceHint(cv accspeccpi.ComponentVersionAccess) string {
+ ref, err := oci.ParseRef(a.ImageReference)
+ if err != nil {
+ return ""
+ }
+ hint := ref.Repository
+ r := cv.Repository()
+ if r != nil {
+ prefix := ocmcpi.RepositoryPrefix(cv.Repository().GetSpecification())
+ if strings.HasPrefix(hint, prefix+grammar.RepositorySeparator) {
+ // try to keep hint identical, even across intermediate
+ // artifact globalizations
+ hint = hint[len(prefix)+1:]
+ }
+ }
+ if ref.Tag != nil {
+ hint += grammar.TagSeparator + *ref.Tag
+ }
+ return hint
+}
+
+func (a *AccessSpec) GetOCIReference(cv accspeccpi.ComponentVersionAccess) (string, error) {
+ return a.ImageReference, nil
+}
+
+func (_ *AccessSpec) GetType() string {
+ return Type
+}
+
+func (a *AccessSpec) AccessMethod(c accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return NewMethod(c.GetContext(), a, a.ImageReference)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type AccessMethodImpl = *accessMethod
+
+type accessMethod struct {
+ lock sync.Mutex
+ ctx accspeccpi.Context
+ spec accspeccpi.AccessSpec
+ reference string
+
+ finalizer Finalizer
+ err error
+
+ id credentials.ConsumerIdentity
+ ref *oci.RefSpec
+ mime string
+ digest digest.Digest
+ art oci.ArtifactAccess
+
+ repo oci.Repository
+ blob artifactset.ArtifactBlob
+}
+
+var (
+ _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+ _ blobaccess.DigestSource = (*accessMethod)(nil)
+ _ accspeccpi.DigestSource = (*accessMethod)(nil)
+ _ credentials.ConsumerIdentityProvider = (*accessMethod)(nil)
+)
+
+func NewMethod(ctx accspeccpi.ContextProvider, a accspeccpi.AccessSpec, ref string, repo ...oci.Repository) (accspeccpi.AccessMethod, error) {
+ m := &accessMethod{
+ spec: a,
+ reference: ref,
+ ctx: ctx.OCMContext(),
+ }
+ return accspeccpi.AccessMethodForImplementation(m, m.eval(general.Optional(repo...)))
+}
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetOCIReference(cv accspeccpi.ComponentVersionAccess) (string, error) {
+ return m.reference, nil
+}
+
+func (m *accessMethod) GetKind() string {
+ return m.spec.GetKind()
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Cache() {
+ m.lock.Lock()
+ ref := m.ref
+ m.lock.Unlock()
+ if ref == nil {
+ return
+ }
+ logger := Logger(WrapContextProvider(m.ctx))
+ logger.Info("cache artifact blob", "ref", m.reference)
+
+ _, m.err = m.getBlob()
+
+ m.finalizer.Finalize()
+}
+
+func (m *accessMethod) Close() error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ list := errors.ErrorList{}
+
+ if m.blob != nil {
+ list.Add(m.blob.Close())
+ }
+ m.blob = nil
+ m.art = nil
+ m.ref = nil
+ list.Add(m.finalizer.Finalize())
+ return list.Result()
+}
+
+func (m *accessMethod) eval(relto oci.Repository) error {
+ var (
+ err error
+ ref oci.RefSpec
+ )
+
+ if relto == nil {
+ ref, err = oci.ParseRef(m.reference)
+ if err != nil {
+ return err
+ }
+ ocictx := m.ctx.OCIContext()
+ spec := ocictx.GetAlias(ref.Host)
+ if spec == nil {
+ spec = ocireg.NewRepositorySpec(ref.Host)
+ }
+ repo, err := ocictx.RepositoryForSpec(spec)
+ if err != nil {
+ return err
+ }
+ m.finalizer.Close(repo, "repository for accessing %s", m.reference)
+ m.repo = repo
+ } else {
+ repo, err := relto.Dup()
+ if err != nil {
+ return err
+ }
+ m.finalizer.Close(repo)
+ art, err := oci.ParseArt(m.reference)
+ if err != nil {
+ return err
+ }
+ ref = oci.RefSpec{
+ UniformRepositorySpec: *repo.GetSpecification().UniformRepositorySpec(),
+ ArtSpec: art,
+ }
+ m.repo = repo
+ }
+
+ m.ref = &ref
+ m.id = credentials.GetProvidedConsumerId(m.repo, credentials.StringUsageContext(ref.Repository))
+ return nil
+}
+
+func (m *accessMethod) GetArtifact() (oci.ArtifactAccess, *oci.RefSpec, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ err := m.getArtifact()
+ if err != nil {
+ return nil, nil, m.err
+ }
+ art := m.art
+ if art != nil {
+ art, err = art.Dup()
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ return art, m.ref, err
+}
+
+func (m *accessMethod) getArtifact() error {
+ if m.art == nil && m.err == nil && m.ref != nil {
+ art, err := m.repo.LookupArtifact(m.ref.Repository, m.ref.Version())
+ m.finalizer.Close(art, "artifact for accessing %s", m.reference)
+ m.art, m.err = art, err
+ if art != nil {
+ m.mime = artdesc.ToContentMediaType(m.art.GetDescriptor().MimeType()) + artifactset.SynthesizedBlobFormat
+ m.digest = art.Digest()
+ }
+ }
+ return m.err
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ return m.id
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return ociidentity.CONSUMER_TYPE
+}
+
+func (m *accessMethod) Digest() digest.Digest {
+ d, _ := m.GetDigest()
+ return d
+}
+
+func (m *accessMethod) GetDigest() (digest.Digest, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ err := m.getArtifact()
+ return m.digest, err
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ blob, err := m.getBlob()
+ if err != nil {
+ return nil, err
+ }
+ return blob.Get()
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ b, err := m.getBlob()
+ if err != nil {
+ return nil, err
+ }
+ r, err := b.Reader()
+ if err != nil {
+ return nil, err
+ }
+ return r, nil
+}
+
+func (m *accessMethod) MimeType() string {
+ if m.mime == "" {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ m.getArtifact()
+ }
+ return m.mime
+}
+
+func (m *accessMethod) getBlob() (artifactset.ArtifactBlob, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil || m.err != nil {
+ return m.blob, m.err
+ }
+
+ err := m.getArtifact()
+ if err != nil {
+ return nil, err
+ }
+ logger := Logger(WrapContextProvider(m.ctx))
+ logger.Info("synthesize artifact blob", "ref", m.reference)
+ m.blob, err = artifactset.SynthesizeArtifactBlobForArtifact(m.art, m.ref.Version())
+ logger.Info("synthesize artifact blob done", "ref", m.reference, "error", logging.ErrorMessage(err))
+ if err != nil {
+ m.err = err
+ return nil, err
+ }
+ return m.blob, nil
+}
diff --git a/api/ocm/extensions/accessmethods/ociartifact/method_test.go b/api/ocm/extensions/accessmethods/ociartifact/method_test.go
new file mode 100644
index 000000000..7aa9d88ee
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociartifact/method_test.go
@@ -0,0 +1,50 @@
+package ociartifact_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+const (
+ OCIPATH = "/tmp/oci"
+ OCIHOST = "alias"
+)
+
+var _ = Describe("Method", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("accesses artifact", func() {
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ OCIManifest1(env)
+ })
+
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ spec := ociartifact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION))
+
+ m, err := spec.AccessMethod(&cpi.DummyComponentVersionAccess{env.OCMContext()})
+ Expect(err).To(Succeed())
+
+ // no credentials required for CTF as fake OCI registry.
+ Expect(credentials.GetProvidedConsumerId(m)).To(BeNil())
+ Expect(accspeccpi.GetAccessMethodImplementation(m).(blobaccess.DigestSource).Digest().String()).To(Equal("sha256:0c4abdb72cf59cb4b77f4aacb4775f9f546ebc3face189b2224a966c8826ca9f"))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/ociartifact/suite_test.go b/api/ocm/extensions/accessmethods/ociartifact/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/ociartifact/suite_test.go
rename to api/ocm/extensions/accessmethods/ociartifact/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/ociartifact/utils.go b/api/ocm/extensions/accessmethods/ociartifact/utils.go
new file mode 100644
index 000000000..55ba75689
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociartifact/utils.go
@@ -0,0 +1,73 @@
+package ociartifact
+
+import (
+ "fmt"
+ "strings"
+
+ "ocm.software/ocm/api/oci/grammar"
+ "ocm.software/ocm/api/ocm/cpi"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+// OCIArtifactReferenceProvider should be implemented by
+// access specs providing access to globally retrievable
+// OCI artifacts.
+type OCIArtifactReferenceProvider interface {
+ // GetOCIReference returns the externally usable OCI reference.
+ // The component version miggt be nil. If it is required to
+ // determine the ref, an appropriate error has to be returned.
+ GetOCIReference(cv cpi.ComponentVersionAccess) (string, error)
+}
+
+func GetOCIArtifactReference(ctx cpi.Context, spec cpi.AccessSpec, cv cpi.ComponentVersionAccess) (string, error) {
+ for spec != nil {
+ eff, err := ctx.AccessSpecForSpec(spec)
+ if err != nil {
+ return "", err
+ }
+ if p, ok := eff.(OCIArtifactReferenceProvider); ok {
+ ref, err := p.GetOCIReference(cv)
+ if ref != "" || err != nil {
+ return ref, err
+ }
+ }
+ spec = cpi.GlobalAccess(spec, ctx)
+ if spec == eff {
+ spec = nil
+ }
+ }
+ return "", nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Hint(nv common.NameVersion, locator, repo, version string) string {
+ if i := strings.LastIndex(version, "@"); i >= 0 {
+ version = version[:i] // remove digest
+ }
+ repository := repoName(nv, locator)
+ if repo != "" {
+ if strings.HasPrefix(repo, grammar.RepositorySeparator) {
+ repository = repo[1:]
+ } else {
+ repository = repoName(nv, repo)
+ }
+ }
+ if repository != "" && version != "" {
+ if !strings.Contains(repository, ":") {
+ repository = fmt.Sprintf("%s:%s", repository, version)
+ }
+ }
+ return repository
+}
+
+func repoName(nv common.NameVersion, locator string) string {
+ if nv.GetName() == "" {
+ return locator
+ } else {
+ if locator == "" {
+ return nv.GetName()
+ }
+ return fmt.Sprintf("%s/%s", nv.GetName(), locator)
+ }
+}
diff --git a/pkg/contexts/ocm/accessmethods/ociblob/README.md b/api/ocm/extensions/accessmethods/ociblob/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/ociblob/README.md
rename to api/ocm/extensions/accessmethods/ociblob/README.md
diff --git a/api/ocm/extensions/accessmethods/ociblob/cli.go b/api/ocm/extensions/accessmethods/ociblob/cli.go
new file mode 100644
index 000000000..f0a7c6007
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociblob/cli.go
@@ -0,0 +1,48 @@
+package ociblob
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.ReferenceOption,
+ options.MediatypeOption,
+ options.SizeOption,
+ options.DigestOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.ReferenceOption, config, "ref")
+ flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType")
+ flagsets.AddFieldByOptionP(opts, options.SizeOption, config, "size")
+ flagsets.AddFieldByOptionP(opts, options.DigestOption, config, "digest")
+ return nil
+}
+
+var usage = `
+This method implements the access of an OCI blob stored in an OCI repository.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **imageReference
** *string*
+
+ OCI repository reference (this artifact name used to store the blob).
+
+- **mediaType
** *string*
+
+ The media type of the blob
+
+- **digest
** *string*
+
+ The digest of the blob used to access the blob in the OCI repository.
+
+- **size
** *integer*
+
+ The size of the blob
+`
diff --git a/api/ocm/extensions/accessmethods/ociblob/method.go b/api/ocm/extensions/accessmethods/ociblob/method.go
new file mode 100644
index 000000000..698227fab
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociblob/method.go
@@ -0,0 +1,193 @@
+package ociblob
+
+import (
+ "fmt"
+ "io"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/credentials"
+ ociidentity "ocm.software/ocm/api/credentials/builtin/oci/identity"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/extensions/repositories/ocireg"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type for a blob in an OCI repository.
+const (
+ Type = "ociBlob"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+}
+
+// New creates a new OCIBlob accessor.
+func New(repository string, digest digest.Digest, mediaType string, size int64) *AccessSpec {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ Reference: repository,
+ MediaType: mediaType,
+ Digest: digest,
+ Size: size,
+ }
+}
+
+// AccessSpec describes the access for a oci registry.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // Reference is the oci reference to the OCI repository
+ Reference string `json:"ref"`
+
+ // MediaType is the media type of the object this schema refers to.
+ MediaType string `json:"mediaType,omitempty"`
+
+ // Digest is the digest of the targeted content.
+ Digest digest.Digest `json:"digest"`
+
+ // Size specifies the size in bytes of the blob.
+ Size int64 `json:"size"`
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("OCI blob %s in repository %s", a.Digest, a.Reference)
+}
+
+func (s *AccessSpec) IsLocal(context accspeccpi.Context) bool {
+ return false
+}
+
+func (s *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return s
+}
+
+func (s *AccessSpec) GetMimeType() string {
+ return s.MediaType
+}
+
+func (s *AccessSpec) AccessMethod(access accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(&accessMethod{comp: access, spec: s}, nil)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO add cache
+
+type accessMethod struct {
+ lock sync.Mutex
+ blob blobaccess.BlobAccess
+ comp accspeccpi.ComponentVersionAccess
+ spec *AccessSpec
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Close() error {
+ var err error
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil {
+ err = m.blob.Close()
+ m.blob = nil
+ }
+ return err
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ return blobaccess.BlobData(m.getBlob())
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ return blobaccess.BlobReader(m.getBlob())
+}
+
+func (m *accessMethod) MimeType() string {
+ return m.spec.MediaType
+}
+
+func (m *accessMethod) getBlob() (blobaccess.BlobAccess, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil {
+ return m.blob, nil
+ }
+ ref, err := oci.ParseRef(m.spec.Reference)
+ if err != nil {
+ return nil, err
+ }
+ if ref.Tag != nil || ref.Digest != nil {
+ return nil, errors.ErrInvalid("oci repository", m.spec.Reference)
+ }
+ ocictx := m.comp.GetContext().OCIContext()
+ spec := ocictx.GetAlias(ref.Host)
+ if spec == nil {
+ spec = ocireg.NewRepositorySpec(ref.Host)
+ }
+ ocirepo, err := m.comp.GetContext().OCIContext().RepositoryForSpec(spec)
+ if err != nil {
+ return nil, err
+ }
+ ns, err := ocirepo.LookupNamespace(ref.Repository)
+ if err != nil {
+ return nil, err
+ }
+ size, acc, err := ns.GetBlobData(m.spec.Digest)
+ if err != nil {
+ return nil, err
+ }
+ if m.spec.Size == blobaccess.BLOB_UNKNOWN_SIZE {
+ m.spec.Size = size
+ } else if size != blobaccess.BLOB_UNKNOWN_SIZE {
+ return nil, errors.Newf("blob size mismatch %d != %d", size, m.spec.Size)
+ }
+ m.blob = blobaccess.ForDataAccess(m.spec.Digest, m.spec.Size, m.spec.MediaType, acc)
+ return m.blob, nil
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ ref, err := oci.ParseRef(m.spec.Reference)
+ if err != nil {
+ return nil
+ }
+
+ ocictx := m.comp.GetContext().OCIContext()
+ spec := ocictx.GetAlias(ref.Host)
+ if spec == nil {
+ spec = ocireg.NewRepositorySpec(ref.Host)
+ }
+ ocirepo, err := m.comp.GetContext().OCIContext().RepositoryForSpec(spec)
+ if err != nil {
+ return nil
+ }
+ return credentials.GetProvidedConsumerId(ocirepo, credentials.StringUsageContext(ref.Repository))
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return ociidentity.CONSUMER_TYPE
+}
diff --git a/api/ocm/extensions/accessmethods/ociblob/method_test.go b/api/ocm/extensions/accessmethods/ociblob/method_test.go
new file mode 100644
index 000000000..a13dab729
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/ociblob/method_test.go
@@ -0,0 +1,50 @@
+package ociblob_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/grammar"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociblob"
+ "ocm.software/ocm/api/utils/accessio"
+)
+
+const (
+ OCIPATH = "/tmp/oci"
+ OCIHOST = "alias"
+)
+
+var _ = Describe("Method", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("accesses artifact", func() {
+ var desc *artdesc.Descriptor
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ desc = OCIManifest1(env)
+ })
+
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ spec := ociblob.New(OCIHOST+".alias"+grammar.RepositorySeparator+OCINAMESPACE, desc.Digest, "", -1)
+
+ m, err := spec.AccessMethod(&cpi.DummyComponentVersionAccess{env.OCMContext()})
+ Expect(err).To(Succeed())
+
+ blob, err := m.Get()
+ Expect(err).To(Succeed())
+
+ Expect(string(blob)).To(Equal("manifestlayer"))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/ociblob/suite_test.go b/api/ocm/extensions/accessmethods/ociblob/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/ociblob/suite_test.go
rename to api/ocm/extensions/accessmethods/ociblob/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/options/doc.go b/api/ocm/extensions/accessmethods/options/doc.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/options/doc.go
rename to api/ocm/extensions/accessmethods/options/doc.go
diff --git a/pkg/contexts/ocm/accessmethods/options/init.go b/api/ocm/extensions/accessmethods/options/init.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/options/init.go
rename to api/ocm/extensions/accessmethods/options/init.go
diff --git a/api/ocm/extensions/accessmethods/options/registry.go b/api/ocm/extensions/accessmethods/options/registry.go
new file mode 100644
index 000000000..f45c5dbb1
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/options/registry.go
@@ -0,0 +1,105 @@
+package options
+
+import (
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/utils"
+)
+
+const (
+ KIND_OPTIONTYPE = "option type"
+ KIND_OPTION = "option"
+)
+
+type OptionTypeCreator func(name string, description string) OptionType
+
+type ValueTypeInfo struct {
+ OptionTypeCreator
+ Description string
+}
+
+func (i ValueTypeInfo) GetDescription() string {
+ return i.Description
+}
+
+type Registry = *registry
+
+var DefaultRegistry = New()
+
+type registry struct {
+ lock sync.RWMutex
+ valueTypes map[string]ValueTypeInfo
+ optionTypes map[string]OptionType
+}
+
+func New() Registry {
+ return ®istry{
+ valueTypes: map[string]ValueTypeInfo{},
+ optionTypes: map[string]OptionType{},
+ }
+}
+
+func (r *registry) RegisterOptionType(t OptionType) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ r.optionTypes[t.GetName()] = t
+}
+
+func (r *registry) RegisterValueType(name string, c OptionTypeCreator, desc string) {
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ r.valueTypes[name] = ValueTypeInfo{OptionTypeCreator: c, Description: desc}
+}
+
+func (r *registry) GetValueType(name string) *ValueTypeInfo {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ if t, ok := r.valueTypes[name]; ok {
+ return &t
+ }
+ return nil
+}
+
+func (r *registry) GetOptionType(name string) OptionType {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ return r.optionTypes[name]
+}
+
+func (r *registry) CreateOptionType(typ, name, desc string) (OptionType, error) {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+ t, ok := r.valueTypes[typ]
+ if !ok {
+ return nil, errors.ErrUnknown(KIND_OPTIONTYPE, typ)
+ }
+
+ n := t.OptionTypeCreator(name, desc)
+ o := r.optionTypes[name]
+ if o != nil {
+ if o.ValueType() != n.ValueType() {
+ return nil, errors.ErrAlreadyExists(KIND_OPTION, name)
+ }
+ return o, nil
+ }
+ return n, nil
+}
+
+func (r *registry) Usage() string {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ tinfo := utils.FormatMap("", r.valueTypes)
+ oinfo := utils.FormatMap("", r.optionTypes)
+
+ return `
+The following predefined option types can be used:
+
+` + oinfo + `
+
+The following predefined value types are supported:
+
+` + tinfo
+}
diff --git a/pkg/contexts/ocm/accessmethods/options/registry_test.go b/api/ocm/extensions/accessmethods/options/registry_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/options/registry_test.go
rename to api/ocm/extensions/accessmethods/options/registry_test.go
diff --git a/pkg/contexts/ocm/accessmethods/options/standard.go b/api/ocm/extensions/accessmethods/options/standard.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/options/standard.go
rename to api/ocm/extensions/accessmethods/options/standard.go
diff --git a/pkg/contexts/ocm/accessmethods/options/suite_test.go b/api/ocm/extensions/accessmethods/options/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/options/suite_test.go
rename to api/ocm/extensions/accessmethods/options/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/options/types.go b/api/ocm/extensions/accessmethods/options/types.go
new file mode 100644
index 000000000..1788c3b80
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/options/types.go
@@ -0,0 +1,118 @@
+package options
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+type OptionType interface {
+ flagsets.ConfigOptionType
+ ValueType() string
+ GetDescriptionText() string
+}
+
+type base = flagsets.ConfigOptionType
+
+type option struct {
+ base
+ valueType string
+}
+
+func (o *option) Equal(t flagsets.ConfigOptionType) bool {
+ if ot, ok := t.(*option); ok {
+ return o.valueType == ot.valueType && o.GetName() == ot.GetName()
+ }
+ return false
+}
+
+func (o *option) ValueType() string {
+ return o.valueType
+}
+
+func (o *option) GetDescription() string {
+ return fmt.Sprintf("[*%s*] %s", o.ValueType(), o.base.GetDescription())
+}
+
+func (o *option) GetDescriptionText() string {
+ return o.base.GetDescription()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewStringOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewStringOptionType(name, desc),
+ valueType: TYPE_STRING,
+ }
+}
+
+func NewStringArrayOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewStringArrayOptionType(name, desc),
+ valueType: TYPE_STRINGARRAY,
+ }
+}
+
+func NewIntOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewIntOptionType(name, desc),
+ valueType: TYPE_INT,
+ }
+}
+
+func NewBoolOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewBoolOptionType(name, desc),
+ valueType: TYPE_BOOL,
+ }
+}
+
+func NewYAMLOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewYAMLOptionType(name, desc),
+ valueType: TYPE_YAML,
+ }
+}
+
+func NewValueMapYAMLOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewValueMapYAMLOptionType(name, desc),
+ valueType: TYPE_STRINGMAPYAML,
+ }
+}
+
+func NewValueMapOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewValueMapOptionType(name, desc),
+ valueType: TYPE_STRING2YAML,
+ }
+}
+
+func NewStringMapOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewStringMapOptionType(name, desc),
+ valueType: TYPE_STRING2STRING,
+ }
+}
+
+func NewStringSliceMapOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewStringSliceMapOptionType(name, desc),
+ valueType: TYPE_STRING2STRINGSLICE,
+ }
+}
+
+func NewStringSliceMapColonOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewStringSliceMapColonOptionType(name, desc),
+ valueType: TYPE_STRINGCOLONSTRINGSLICE,
+ }
+}
+
+func NewBytesOptionType(name, desc string) OptionType {
+ return &option{
+ base: flagsets.NewBytesOptionType(name, desc),
+ valueType: TYPE_BYTES,
+ }
+}
diff --git a/api/ocm/extensions/accessmethods/plugin/cmd_test.go b/api/ocm/extensions/accessmethods/plugin/cmd_test.go
new file mode 100644
index 000000000..f7d588f12
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/plugin/cmd_test.go
@@ -0,0 +1,79 @@
+package plugin_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/env"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
+
+ "github.com/mandelsoft/goutils/sliceutils"
+ "github.com/mandelsoft/goutils/transformer"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+const (
+ CA = "/tmp/ca"
+ VERSION = "v1"
+)
+
+var _ = Describe("Add with new access method", func() {
+ var env *Environment
+ var ctx ocm.Context
+ var registry plugins.Set
+ var plugins TempPluginDir
+
+ BeforeEach(func() {
+ env = NewEnvironment(TestData())
+ ctx = env.OCMContext()
+ plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata"))
+ Expect(registration.RegisterExtensions(ctx)).To(Succeed())
+ p := registry.Get("test")
+ Expect(p).NotTo(BeNil())
+ })
+
+ AfterEach(func() {
+ plugins.Cleanup()
+ env.Cleanup()
+ })
+
+ It("handles resource options", func() {
+ at := ctx.AccessMethods().GetType("test")
+ Expect(at).NotTo(BeNil())
+
+ h := at.ConfigOptionTypeSetHandler()
+ Expect(h).NotTo(BeNil())
+ Expect(h.GetName()).To(Equal("test"))
+
+ ot := h.OptionTypes()
+ Expect(len(ot)).To(Equal(2))
+
+ opts := h.CreateOptions()
+ Expect(sliceutils.Transform(opts.Options(), transformer.GetName[flagsets.Option, string])).To(ConsistOf(
+ "mediaType", "accessPath"))
+
+ fs := &pflag.FlagSet{}
+ fs.SortFlags = true
+ opts.AddFlags(fs)
+
+ Expect("\n" + fs.FlagUsages()).To(Equal(`
+ --accessPath string file path
+ --mediaType string media type for artifact blob representation
+`))
+
+ MustBeSuccessful(fs.Parse([]string{"--accessPath", "filepath", "--" + options.MediatypeOption.GetName(), "yaml"}))
+
+ cfg := flagsets.Config{}
+ MustBeSuccessful(h.ApplyConfig(opts, cfg))
+ Expect(cfg).To(YAMLEqual(`
+mediaType: yaml
+path: filepath
+`))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/plugin/doc.go b/api/ocm/extensions/accessmethods/plugin/doc.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/plugin/doc.go
rename to api/ocm/extensions/accessmethods/plugin/doc.go
diff --git a/api/ocm/extensions/accessmethods/plugin/method.go b/api/ocm/extensions/accessmethods/plugin/method.go
new file mode 100644
index 000000000..95f8e32ee
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/plugin/method.go
@@ -0,0 +1,144 @@
+package plugin
+
+import (
+ "encoding/json"
+ "io"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/ocm"
+ cpi "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type AccessSpec struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+ handler *PluginHandler
+}
+
+var (
+ _ cpi.AccessSpec = &AccessSpec{}
+ _ cpi.HintProvider = &AccessSpec{}
+)
+
+func (s *AccessSpec) AccessMethod(cv cpi.ComponentVersionAccess) (cpi.AccessMethod, error) {
+ return s.handler.AccessMethod(s, cv)
+}
+
+func (s *AccessSpec) Describe(ctx cpi.Context) string {
+ return s.handler.Describe(s, ctx)
+}
+
+func (_ *AccessSpec) IsLocal(cpi.Context) bool {
+ return false
+}
+
+func (s *AccessSpec) GlobalAccessSpec(cpi.Context) cpi.AccessSpec {
+ return s
+}
+
+func (s *AccessSpec) GetMimeType() string {
+ return s.handler.GetMimeType(s)
+}
+
+func (s *AccessSpec) GetReferenceHint(cv cpi.ComponentVersionAccess) string {
+ return s.handler.GetReferenceHint(s, cv)
+}
+
+func (s *AccessSpec) Handler() *PluginHandler {
+ return s.handler
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type accessMethod struct {
+ lock sync.Mutex
+ blob blobaccess.BlobAccess
+ ctx ocm.Context
+
+ handler *PluginHandler
+ spec *AccessSpec
+ info *ppi.AccessSpecInfo
+ creds json.RawMessage
+}
+
+var _ cpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func newMethod(p *PluginHandler, spec *AccessSpec, ctx ocm.Context, info *ppi.AccessSpecInfo, creds json.RawMessage) *accessMethod {
+ return &accessMethod{
+ ctx: ctx,
+ handler: p,
+ spec: spec,
+ info: info,
+ creds: creds,
+ }
+}
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return m.spec.GetKind()
+}
+
+func (m *accessMethod) AccessSpec() cpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Close() error {
+ var err error
+ m.lock.Lock()
+ defer m.lock.Unlock()
+ if m.blob != nil {
+ err = m.blob.Close()
+ m.blob = nil
+ }
+ return err
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ return blobaccess.BlobData(m.getBlob())
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ return blobaccess.BlobReader(m.getBlob())
+}
+
+func (m *accessMethod) MimeType() string {
+ return m.info.MediaType
+}
+
+func (m *accessMethod) getBlob() (blobaccess.BlobAccess, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil {
+ return m.blob, nil
+ }
+
+ spec, err := json.Marshal(m.spec)
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot marshal access spec")
+ }
+ m.blob = accessobj.CachedBlobAccessForWriter(m.ctx, m.MimeType(), plugin.NewAccessDataWriter(m.handler.plug, m.creds, spec))
+ return m.blob, nil
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ if len(m.info.ConsumerId) == 0 {
+ return nil
+ }
+ return m.info.ConsumerId
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return hostpath.IDENTITY_TYPE
+}
diff --git a/api/ocm/extensions/accessmethods/plugin/method_test.go b/api/ocm/extensions/accessmethods/plugin/method_test.go
new file mode 100644
index 000000000..42f891928
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/plugin/method_test.go
@@ -0,0 +1,84 @@
+//go:build unix
+
+package plugin_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
+
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+const (
+ ARCH = "ctf"
+ COMP = "github.com/mandelsoft/comp"
+ VERS = "1.0.0"
+ PROVIDER = "mandelsoft"
+)
+
+var _ = Describe("setup plugin cache", func() {
+ var ctx ocm.Context
+ var registry plugins.Set
+ var env *Builder
+ var plugins TempPluginDir
+
+ var accessSpec ocm.AccessSpec
+
+ BeforeEach(func() {
+ var err error
+
+ accessSpec, err = ocm.NewGenericAccessSpec(`
+type: test
+someattr: value
+`)
+ Expect(err).To(Succeed())
+
+ env = NewBuilder(nil)
+ ctx = env.OCMContext()
+ plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata"))
+ Expect(registration.RegisterExtensions(ctx)).To(Succeed())
+ p := registry.Get("test")
+ Expect(p).NotTo(BeNil())
+ })
+
+ AfterEach(func() {
+ plugins.Cleanup()
+ env.Cleanup()
+ })
+
+ It("registers access methods", func() {
+ env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() {
+ env.Component(COMP, func() {
+ env.Version(VERS, func() {
+ env.Provider(PROVIDER)
+ env.Resource("testdata", VERS, "PlainText", metav1.ExternalRelation, func() {
+ env.Access(accessSpec)
+ })
+ })
+ })
+ })
+
+ repo := Must(ctf.Open(ctx, accessobj.ACC_READONLY, ARCH, 0, env))
+ defer Close(repo)
+
+ cv := Must(repo.LookupComponentVersion(COMP, VERS))
+ defer Close(cv)
+
+ r := Must(cv.GetResourceByIndex(0))
+
+ m := Must(r.AccessMethod())
+ Expect(m.MimeType()).To(Equal("plain/text"))
+
+ data := Must(m.Get())
+ Expect(string(data)).To(Equal("test content\n{\"someattr\":\"value\",\"type\":\"test\"}\n"))
+ })
+})
diff --git a/api/ocm/extensions/accessmethods/plugin/plugin.go b/api/ocm/extensions/accessmethods/plugin/plugin.go
new file mode 100644
index 000000000..a6465a722
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/plugin/plugin.go
@@ -0,0 +1,134 @@
+package plugin
+
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/utils/errkind"
+)
+
+type plug = plugin.Plugin
+
+// PluginHandler is a shared object between the AccessMethod implementation and the AccessSpec implementation. The
+// object knows the actual plugin and can therefore forward the method calls to corresponding cli commands.
+type PluginHandler struct {
+ plug
+
+ // cached info
+ info *ppi.AccessSpecInfo
+ err error
+ orig []byte
+}
+
+func NewPluginHandler(p plugin.Plugin) *PluginHandler {
+ return &PluginHandler{plug: p}
+}
+
+func (p *PluginHandler) Info(spec *AccessSpec) (*ppi.AccessSpecInfo, error) {
+ if p.info != nil || p.err != nil {
+ raw, err := spec.UnstructuredVersionedTypedObject.GetRaw()
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot marshal access specification")
+ }
+ if bytes.Equal(raw, p.orig) {
+ return p.info, p.err
+ }
+ }
+ p.info, p.err = p.Validate(spec)
+ return p.info, p.err
+}
+
+func (p *PluginHandler) AccessMethod(spec *AccessSpec, cv cpi.ComponentVersionAccess) (cpi.AccessMethod, error) {
+ mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion())
+ if mspec == nil {
+ return nil, errors.ErrNotFound(errkind.KIND_ACCESSMETHOD, spec.GetType(), descriptor.KIND_PLUGIN, p.Name())
+ }
+
+ creddata, err := p.getCredentialData(spec, cv)
+ if err != nil {
+ return nil, err
+ }
+
+ info, err := p.Info(spec)
+ if err != nil {
+ return nil, err
+ }
+ return accspeccpi.AccessMethodForImplementation(newMethod(p, spec, cv.GetContext(), info, creddata), nil)
+}
+
+func (p *PluginHandler) getCredentialData(spec *AccessSpec, cv cpi.ComponentVersionAccess) (json.RawMessage, error) {
+ info, err := p.Info(spec)
+ if err != nil {
+ return nil, err
+ }
+
+ var creds credentials.Credentials
+ if len(info.ConsumerId) > 0 {
+ creds, err = credentials.CredentialsForConsumer(cv.GetContext(), info.ConsumerId, hostpath.IdentityMatcher(info.ConsumerId.Type()))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var creddata json.RawMessage
+ if creds != nil {
+ creddata, err = json.Marshal(creds)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return creddata, nil
+}
+
+func (p *PluginHandler) Describe(spec *AccessSpec, ctx cpi.Context) string {
+ mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion())
+ if mspec == nil {
+ return "unknown type " + spec.GetType()
+ }
+ info, err := p.Info(spec)
+ if err != nil {
+ return err.Error()
+ }
+ return info.Short
+}
+
+func (p *PluginHandler) GetMimeType(spec *AccessSpec) string {
+ mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion())
+ if mspec == nil {
+ return "unknown type " + spec.GetType()
+ }
+ info, err := p.Info(spec)
+ if err != nil {
+ return ""
+ }
+ return info.Short
+}
+
+func (p *PluginHandler) GetReferenceHint(spec *AccessSpec, cv cpi.ComponentVersionAccess) string {
+ mspec := p.GetAccessMethodDescriptor(spec.GetKind(), spec.GetVersion())
+ if mspec == nil {
+ return "unknown type " + spec.GetType()
+ }
+ info, err := p.Info(spec)
+ if err != nil {
+ return ""
+ }
+ return info.Hint
+}
+
+func (p *PluginHandler) Validate(spec *AccessSpec) (*ppi.AccessSpecInfo, error) {
+ data, err := spec.GetRaw()
+ if err != nil {
+ return nil, err
+ }
+ return p.plug.ValidateAccessMethod(data)
+}
diff --git a/pkg/contexts/ocm/accessmethods/plugin/suite_test.go b/api/ocm/extensions/accessmethods/plugin/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/plugin/suite_test.go
rename to api/ocm/extensions/accessmethods/plugin/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/plugin/testdata/test b/api/ocm/extensions/accessmethods/plugin/testdata/test
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/plugin/testdata/test
rename to api/ocm/extensions/accessmethods/plugin/testdata/test
diff --git a/api/ocm/extensions/accessmethods/plugin/type.go b/api/ocm/extensions/accessmethods/plugin/type.go
new file mode 100644
index 000000000..9f9638594
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/plugin/type.go
@@ -0,0 +1,69 @@
+package plugin
+
+import (
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type accessType struct {
+ accspeccpi.AccessType
+ plug plugin.Plugin
+ cliopts flagsets.ConfigOptionTypeSet
+}
+
+var _ accspeccpi.AccessType = (*accessType)(nil)
+
+func NewType(name string, p plugin.Plugin, desc *plugin.AccessMethodDescriptor) accspeccpi.AccessType {
+ format := desc.Format
+ if format != "" {
+ format = "\n" + format
+ }
+
+ t := &accessType{
+ plug: p,
+ }
+
+ cfghdlr := flagsets.NewConfigOptionTypeSetHandler(name, t.AddConfig)
+ for _, o := range desc.CLIOptions {
+ var opt flagsets.ConfigOptionType
+ if o.Type == "" {
+ opt = options.DefaultRegistry.GetOptionType(o.Name)
+ if opt == nil {
+ p.Context().Logger(plugin.TAG).Warn("unknown option", "plugin", p.Name(), "accessmethod", name, "option", o.Name)
+ }
+ } else {
+ var err error
+ opt, err = options.DefaultRegistry.CreateOptionType(o.Type, o.Name, o.Description)
+ if err != nil {
+ p.Context().Logger(plugin.TAG).Warn("invalid option", "plugin", p.Name(), "accessmethod", name, "option", o.Name, "error", err.Error())
+ }
+ }
+ if opt != nil {
+ cfghdlr.AddOptionType(opt)
+ }
+ }
+ aopts := []accspeccpi.AccessSpecTypeOption{accspeccpi.WithDescription(desc.Description), accspeccpi.WithFormatSpec(format)}
+ if cfghdlr.Size() > 0 {
+ aopts = append(aopts, accspeccpi.WithConfigHandler(cfghdlr))
+ t.cliopts = cfghdlr
+ }
+ t.AccessType = accspeccpi.NewAccessSpecType[*AccessSpec](name, aopts...)
+ return t
+}
+
+func (t *accessType) Decode(data []byte, unmarshaler runtime.Unmarshaler) (accspeccpi.AccessSpec, error) {
+ spec, err := t.AccessType.Decode(data, unmarshaler)
+ if err != nil {
+ return nil, err
+ }
+ spec.(*AccessSpec).handler = NewPluginHandler(t.plug)
+ return spec, nil
+}
+
+func (t *accessType) AddConfig(opts flagsets.ConfigOptions, cfg flagsets.Config) error {
+ opts = opts.FilterBy(t.cliopts.HasOptionType)
+ return t.plug.ComposeAccessMethod(t.GetType(), opts, cfg)
+}
diff --git a/pkg/contexts/ocm/accessmethods/relativeociref/README.md b/api/ocm/extensions/accessmethods/relativeociref/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/relativeociref/README.md
rename to api/ocm/extensions/accessmethods/relativeociref/README.md
diff --git a/api/ocm/extensions/accessmethods/relativeociref/method.go b/api/ocm/extensions/accessmethods/relativeociref/method.go
new file mode 100644
index 000000000..69a28664a
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/relativeociref/method.go
@@ -0,0 +1,88 @@
+package relativeociref
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/ocm/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type describes the access of an OCI artifact stored as OCI artifact in the OCI
+// registry hosting the actual component version.
+const (
+ Type = "relativeOciReference"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1))
+}
+
+var _ accspeccpi.HintProvider = (*AccessSpec)(nil)
+
+// New creates a new localFilesystemBlob accessor.
+func New(ref string) *AccessSpec {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedObjectType(Type),
+ Reference: ref,
+ }
+}
+
+// AccessSpec describes the access of an OCI artifact stored as OCI artifact in
+// the OCI registry hosting the actual component version.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ // Reference is the OCI repository name and version separated by a colon.
+ Reference string `json:"reference"`
+}
+
+func (a *AccessSpec) Describe(context accspeccpi.Context) string {
+ return fmt.Sprintf("local OCI artifact %s", a.Reference)
+}
+
+func (a *AccessSpec) IsLocal(context accspeccpi.Context) bool {
+ return true
+}
+
+func (a *AccessSpec) GlobalAccessSpec(context accspeccpi.Context) accspeccpi.AccessSpec {
+ return nil
+}
+
+func (a *AccessSpec) AccessMethod(access accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return access.AccessMethod(a)
+}
+
+func (a *AccessSpec) GetDigest() (string, bool) {
+ ref, err := oci.ParseRef(a.Reference)
+ if err != nil {
+ return "", true
+ }
+ if ref.Digest != nil {
+ return ref.Digest.String(), true
+ }
+ return "", false
+}
+
+func (a *AccessSpec) GetReferenceHint(cv internal.ComponentVersionAccess) string {
+ return a.Reference
+}
+
+func (a *AccessSpec) GetOCIReference(cv accspeccpi.ComponentVersionAccess) (string, error) {
+ if cv == nil {
+ return "", fmt.Errorf("component version required to determine OCI reference")
+ }
+ m, err := a.AccessMethod(cv)
+ if err != nil {
+ return "", err
+ }
+ defer m.Close()
+
+ if o, ok := accspeccpi.GetAccessMethodImplementation(m).(ociartifact.OCIArtifactReferenceProvider); ok {
+ return o.GetOCIReference(nil)
+ }
+ return "", nil
+}
diff --git a/api/ocm/extensions/accessmethods/relativeociref/method_test.go b/api/ocm/extensions/accessmethods/relativeociref/method_test.go
new file mode 100644
index 000000000..4634480fc
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/relativeociref/method_test.go
@@ -0,0 +1,75 @@
+package relativeociref_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "github.com/mandelsoft/goutils/finalizer"
+
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/relativeociref"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ utils "ocm.software/ocm/api/ocm/ocmutils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+const (
+ OCIPATH = "/tmp/oci"
+ OCIHOST = "alias"
+)
+
+const (
+ COMP = "acme.org/compo"
+ COMPVERS = "v1.0.0"
+ RES = "ref"
+)
+
+var _ = Describe("Method", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("accesses artifact", func() {
+ var finalize finalizer.Finalizer
+ defer Defer(finalize.Finalize)
+
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ OCIManifest1(env)
+ })
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ env.OCMCommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMP, COMPVERS, func() {
+ env.Resource(RES, COMPVERS, "testtyp", v1.LocalRelation, func() {
+ env.Access(relativeociref.New(OCINAMESPACE + ":" + OCIVERSION))
+ })
+ })
+ })
+
+ repo := Must(ctf.Open(env, accessobj.ACC_READONLY, OCIPATH, 0, env))
+ finalize.Close(repo)
+ vers := Must(repo.LookupComponentVersion(COMP, COMPVERS))
+ finalize.Close(vers)
+ res := Must(vers.GetResourceByIndex(0))
+ m := Must(res.AccessMethod())
+ finalize.With(func() error {
+ return m.Close()
+ })
+ data := Must(m.Get())
+ Expect(len(data)).To(Equal(628))
+ Expect(accspeccpi.GetAccessMethodImplementation(m).(blobaccess.DigestSource).Digest().String()).To(Equal("sha256:0c4abdb72cf59cb4b77f4aacb4775f9f546ebc3face189b2224a966c8826ca9f"))
+ Expect(utils.GetOCIArtifactRef(env, res)).To(Equal("ocm/value:v2.0"))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/relativeociref/suite_test.go b/api/ocm/extensions/accessmethods/relativeociref/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/relativeociref/suite_test.go
rename to api/ocm/extensions/accessmethods/relativeociref/suite_test.go
diff --git a/api/ocm/extensions/accessmethods/relativeociref/transfer_test.go b/api/ocm/extensions/accessmethods/relativeociref/transfer_test.go
new file mode 100644
index 000000000..acdcd3a93
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/relativeociref/transfer_test.go
@@ -0,0 +1,149 @@
+package relativeociref_test
+
+import (
+ "encoding/json"
+ "fmt"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ ocictf "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/relativeociref"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ storagecontext "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci/ocirepo"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/tools/transfer"
+ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+const OUT = "/tmp/res"
+
+func FakeOCIRegBaseFunction(ctx *storagecontext.StorageContext) string {
+ return "baseurl.io"
+}
+
+var _ = Describe("Transfer handler", func() {
+ var env *Builder
+ var ldesc *artdesc.Descriptor
+
+ BeforeEach(func() {
+ env = NewBuilder()
+
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ ldesc = OCIManifest1(env)
+ })
+
+ env.OCMCommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMP, COMPVERS, func() {
+ env.Resource("artifact", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() {
+ env.Access(
+ relativeociref.New(OCINAMESPACE + ":" + OCIVERSION),
+ )
+ })
+ })
+ })
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("it should copy an image by value to a ctf file", func() {
+ src, err := ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, OCIPATH, 0, env)
+ Expect(err).To(Succeed())
+ cv, err := src.LookupComponentVersion(COMP, COMPVERS)
+ Expect(err).To(Succeed())
+ tgt, err := ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env)
+ Expect(err).To(Succeed())
+ defer tgt.Close()
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+ // handler, err := standard.New(standard.ResourcesByValue())
+ Expect(err).To(Succeed())
+ err = transfer.TransferVersion(nil, nil, cv, tgt, handler)
+ Expect(err).To(Succeed())
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list, err := tgt.ComponentLister().GetComponents("", true)
+ Expect(err).To(Succeed())
+ Expect(list).To(Equal([]string{COMP}))
+ comp, err := tgt.LookupComponentVersion(COMP, COMPVERS)
+ Expect(err).To(Succeed())
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(1))
+ Expect(comp.GetDescriptor().Resources[0].Access.GetType()).To(Equal(localblob.Type))
+ data, err := json.Marshal(comp.GetDescriptor().Resources[0].Access)
+ Expect(err).To(Succeed())
+
+ fmt.Printf("%s\n", string(data))
+ hash := HashManifest1(artifactset.DefaultArtifactSetDescriptorFileName)
+ Expect(string(data)).To(StringEqualWithContext(fmt.Sprintf(`{"localReference":"%s","mediaType":"application/vnd.oci.image.manifest.v1+tar+gzip","referenceName":"ocm/value:v2.0","type":"localBlob"}`, hash)))
+
+ r, err := comp.GetResourceByIndex(0)
+ Expect(err).To(Succeed())
+ meth, err := r.AccessMethod()
+ Expect(err).To(Succeed())
+ defer meth.Close()
+ reader, err := meth.Reader()
+ Expect(err).To(Succeed())
+ defer reader.Close()
+ set, err := artifactset.Open(accessobj.ACC_READONLY, "", 0, accessio.Reader(reader))
+ Expect(err).To(Succeed())
+ defer set.Close()
+
+ _, blob, err := set.GetBlobData(ldesc.Digest)
+ Expect(err).To(Succeed())
+ data, err = blob.Get()
+ Expect(err).To(Succeed())
+ Expect(string(data)).To(Equal("manifestlayer"))
+ })
+
+ It("it should copy an image by value to an oci repo with uploader", func() {
+ env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction),
+ cpi.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), cpi.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)))
+ keepblobattr.Set(env.OCMContext(), true)
+
+ src, err := ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, OCIPATH, 0, env)
+ Expect(err).To(Succeed())
+ cv, err := src.LookupComponentVersion(COMP, COMPVERS)
+ Expect(err).To(Succeed())
+
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer tgt.Close()
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+ // handler, err := standard.New(standard.ResourcesByValue())
+ Expect(err).To(Succeed())
+ err = transfer.TransferVersion(nil, nil, cv, tgt, handler)
+ Expect(err).To(Succeed())
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list, err := tgt.ComponentLister().GetComponents("", true)
+ Expect(err).To(Succeed())
+ Expect(list).To(Equal([]string{COMP}))
+ comp, err := tgt.LookupComponentVersion(COMP, COMPVERS)
+ Expect(err).To(Succeed())
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(1))
+ Expect(comp.GetDescriptor().Resources[0].Access.GetType()).To(Equal(localblob.Type))
+ data, err := json.Marshal(comp.GetDescriptor().Resources[0].Access)
+ Expect(err).To(Succeed())
+
+ fmt.Printf("%s\n", string(data))
+ hash := HashManifest1(artifactset.DefaultArtifactSetDescriptorFileName)
+ Expect(string(data)).To(StringEqualWithContext(fmt.Sprintf(`{"globalAccess":{"imageReference":"baseurl.io/ocm/value:v2.0","type":"ociArtifact"},"localReference":"%s","mediaType":"application/vnd.oci.image.manifest.v1+tar+gzip","referenceName":"ocm/value:v2.0","type":"localBlob"}`, hash)))
+ })
+})
diff --git a/pkg/contexts/ocm/accessmethods/s3/README.md b/api/ocm/extensions/accessmethods/s3/README.md
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/s3/README.md
rename to api/ocm/extensions/accessmethods/s3/README.md
diff --git a/api/ocm/extensions/accessmethods/s3/cli.go b/api/ocm/extensions/accessmethods/s3/cli.go
new file mode 100644
index 000000000..07ee17776
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/s3/cli.go
@@ -0,0 +1,30 @@
+package s3
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.RegionOption,
+ options.BucketOption,
+ options.ReferenceOption,
+ options.MediatypeOption,
+ options.VersionOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.ReferenceOption, config, "key")
+ flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType")
+ flagsets.AddFieldByOptionP(opts, options.RegionOption, config, "region")
+ flagsets.AddFieldByOptionP(opts, options.BucketOption, config, "bucket")
+ flagsets.AddFieldByOptionP(opts, options.VersionOption, config, "version")
+ return nil
+}
+
+var usage = `
+This method implements the access of a blob stored in an S3 bucket.
+`
diff --git a/api/ocm/extensions/accessmethods/s3/identity/identity.go b/api/ocm/extensions/accessmethods/s3/identity/identity.go
new file mode 100644
index 000000000..327e2a398
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/s3/identity/identity.go
@@ -0,0 +1,66 @@
+package identity
+
+import (
+ "path"
+ "strings"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+const CONSUMER_TYPE = "S3"
+
+// identity properties.
+const (
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+)
+
+// credential properties.
+const (
+ ATTR_AWS_ACCESS_KEY_ID = "awsAccessKeyID"
+ ATTR_AWS_SECRET_ACCESS_KEY = "awsSecretAccessKey"
+ ATTR_TOKEN = cpi.ATTR_TOKEN
+)
+
+const GITHUB = "github.com"
+
+var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)
+
+func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
+ return identityMatcher(pattern, cur, id)
+}
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_AWS_ACCESS_KEY_ID, "AWS access key id",
+ ATTR_AWS_SECRET_ACCESS_KEY, "AWS secret for access key id",
+ ATTR_TOKEN, "AWS access token (alternatively)",
+ })
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, identityMatcher,
+ `S3 credential matcher
+
+This matcher is a hostpath matcher.`,
+ attrs)
+}
+
+func GetConsumerId(host, bucket, key, version string) cpi.ConsumerIdentity {
+ id := cpi.NewConsumerIdentity(CONSUMER_TYPE)
+
+ parts := strings.Split(host, ":")
+ if parts[0] != "" {
+ id[ID_HOSTNAME] = parts[0]
+ }
+ if len(parts) > 1 {
+ id[ID_PORT] = parts[1]
+ }
+ id[ID_PATHPREFIX] = path.Join(bucket, key, version)
+ return id
+}
+
+func GetCredentials(ctx cpi.ContextProvider, host, bucket, key, version string) (cpi.Credentials, error) {
+ id := GetConsumerId(host, bucket, key, version)
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id, identityMatcher)
+}
diff --git a/api/ocm/extensions/accessmethods/s3/ifce_test.go b/api/ocm/extensions/accessmethods/s3/ifce_test.go
new file mode 100644
index 000000000..75631bc3d
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/s3/ifce_test.go
@@ -0,0 +1,9 @@
+package s3
+
+import (
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+)
+
+func Versions() accspeccpi.AccessTypeVersionScheme {
+ return versions
+}
diff --git a/api/ocm/extensions/accessmethods/s3/method.go b/api/ocm/extensions/accessmethods/s3/method.go
new file mode 100644
index 000000000..728ae81d6
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/s3/method.go
@@ -0,0 +1,175 @@
+package s3
+
+import (
+ "fmt"
+
+ . "github.com/mandelsoft/goutils/exception"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/s3/identity"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessio/downloader"
+ "ocm.software/ocm/api/utils/accessio/downloader/s3"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type of S3 registry.
+const (
+ Type = "s3"
+
+ LegacyType = "S3"
+ LegacyTypeV1 = LegacyType + runtime.VersionSeparator + "v1"
+)
+
+var versions = accspeccpi.NewAccessTypeVersionScheme(Type).WithKindAliases(LegacyType)
+
+var formats = accspeccpi.NewAccessSpecFormatVersionRegistry()
+
+func init() {
+ formats.Register(Type, runtime.NewConvertedVersion[accspeccpi.AccessSpec, *AccessSpec, *AccessSpecV1](&converterV1{}))
+ formats.Register(LegacyType, runtime.NewConvertedVersion[accspeccpi.AccessSpec, *AccessSpec, *AccessSpecV1](&converterV1{}))
+
+ initV1()
+ initV2()
+
+ anon := accspeccpi.MustNewAccessSpecMultiFormatVersion(Type, formats)
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByFormatVersion(Type, anon, accspeccpi.WithDescription(usage), accspeccpi.WithConfigHandler(ConfigHandler()))))
+ Must(versions.Register(accspeccpi.NewAccessSpecTypeByFormatVersion(LegacyType, anon, accspeccpi.WithDescription(usage))))
+ accspeccpi.RegisterAccessTypeVersions(versions)
+}
+
+// AccessSpec describes the access for a S3 registry.
+type AccessSpec struct {
+ runtime.InternalVersionedTypedObject[accspeccpi.AccessSpec]
+
+ // Region needs to be set even though buckets are global.
+ // We can't assume that there is a default region setting sitting somewhere.
+ // +optional
+ Region string
+ // Bucket where the s3 object is located.
+ Bucket string
+ // Key of the object to look for. This value will be used together with Bucket and Version to form an identity.
+ Key string
+ // Version of the object.
+ // +optional
+ Version string
+ // MediaType defines the mime type of the object to download.
+ // +optional
+ MediaType string
+ downloader downloader.Downloader
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+// New creates a new GitHub registry access spec version v1.
+func New(region, bucket, key, version, mediaType string, downloader ...downloader.Downloader) *AccessSpec {
+ return &AccessSpec{
+ InternalVersionedTypedObject: runtime.NewInternalVersionedTypedObject[accspeccpi.AccessSpec](versions, Type),
+ Region: region,
+ Bucket: bucket,
+ Key: key,
+ Version: version,
+ MediaType: mediaType,
+ downloader: utils.Optional(downloader...),
+ }
+}
+
+func (a AccessSpec) MarshalJSON() ([]byte, error) {
+ return runtime.MarshalVersionedTypedObject(&a)
+}
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("S3 key %s in bucket %s", a.Key, a.Bucket)
+}
+
+func (_ *AccessSpec) IsLocal(accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+func (a *AccessSpec) AccessMethod(c accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(newMethod(c, a))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type accessMethod struct {
+ blobaccess.BlobAccess
+
+ comp accspeccpi.ComponentVersionAccess
+ spec *AccessSpec
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (*accessMethod, error) {
+ creds, err := getCreds(a, c.GetContext().CredentialsContext())
+ if err != nil {
+ return nil, fmt.Errorf("failed to get creds: %w", err)
+ }
+
+ var (
+ accessKeyID string
+ accessSecret string
+ )
+ if creds != nil {
+ accessKeyID = creds.GetProperty(identity.ATTR_AWS_ACCESS_KEY_ID)
+ accessSecret = creds.GetProperty(identity.ATTR_AWS_SECRET_ACCESS_KEY)
+ }
+ var awsCreds *s3.AWSCreds
+ if accessKeyID != "" {
+ awsCreds = &s3.AWSCreds{
+ AccessKeyID: accessKeyID,
+ AccessSecret: accessSecret,
+ }
+ }
+ d := a.downloader
+ if d == nil {
+ d = s3.NewDownloader(a.Region, a.Bucket, a.Key, a.Version, awsCreds)
+ }
+ w := accessio.NewWriteAtWriter(d.Download)
+ // don't change the spec, leave it empty.
+ mediaType := a.MediaType
+ if mediaType == "" {
+ mediaType = mime.MIME_OCTET
+ }
+ cacheBlobAccess := accessobj.CachedBlobAccessForWriter(c.GetContext(), mediaType, w)
+ return &accessMethod{
+ spec: a,
+ comp: c,
+ BlobAccess: cacheBlobAccess,
+ }, nil
+}
+
+func getCreds(a *AccessSpec, cctx credentials.Context) (credentials.Credentials, error) {
+ return identity.GetCredentials(cctx, "", a.Bucket, a.Key, a.Version)
+}
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ return identity.GetConsumerId("", m.spec.Bucket, m.spec.Key, m.spec.Version)
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return hostpath.IDENTITY_TYPE
+}
diff --git a/api/ocm/extensions/accessmethods/s3/method_test.go b/api/ocm/extensions/accessmethods/s3/method_test.go
new file mode 100644
index 000000000..d8bd556aa
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/s3/method_test.go
@@ -0,0 +1,200 @@
+package s3_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "github.com/mandelsoft/filepath/pkg/filepath"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/attrs/tmpcache"
+ "ocm.software/ocm/api/datacontext/attrs/vfsattr"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/s3"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/s3/identity"
+ "ocm.software/ocm/api/utils/accessio/downloader"
+)
+
+type mockDownloader struct {
+ expected []byte
+ err error
+}
+
+func (m *mockDownloader) Download(w io.WriterAt) error {
+ if _, err := w.WriteAt(m.expected, 0); err != nil {
+ return fmt.Errorf("failed to write to mock writer: %w", err)
+ }
+ return m.err
+}
+
+func checkMarshal(spec *s3.AccessSpec, typ string, fmt string) {
+ if typ != "" {
+ spec.SetType(typ)
+ }
+ data := MustWithOffset(1, Calling(json.Marshal(spec)))
+ ExpectWithOffset(1, string(data)).To(Equal(fmt))
+
+ n := MustWithOffset(1, Calling(ocm.DefaultContext().AccessSpecForConfig(data, nil)))
+ Expect(reflect.TypeOf(n)).To(Equal(reflect.TypeOf(spec)))
+ Expect(n.GetType()).To(Equal(general.Conditional(typ == "", s3.Type, typ)))
+ data2 := Must(json.Marshal(n))
+ ExpectWithOffset(1, string(data2)).To(StringEqualWithContext(string(data)))
+}
+
+func checkDecode(spec *s3.AccessSpec, typ string, fmt string) {
+ if typ != "" {
+ spec.SetType(typ)
+ }
+ data := MustWithOffset(1, Calling(json.Marshal(spec)))
+
+ n := MustWithOffset(1, Calling(s3.Versions().Decode([]byte(fmt), nil)))
+ Expect(reflect.TypeOf(n)).To(Equal(reflect.TypeOf(spec)))
+
+ data2 := Must(json.Marshal(n))
+ ExpectWithOffset(1, string(data2)).To(StringEqualWithContext(string(data)))
+}
+
+var _ = Describe("Method", func() {
+ Context("specification", func() {
+ var spec *s3.AccessSpec
+
+ BeforeEach(func() {
+ spec = s3.New(
+ "region",
+ "bucket",
+ "key",
+ "version",
+ "tar/gz",
+ )
+ })
+
+ It("serializes", func() {
+ checkMarshal(spec, "", "{\"type\":\"s3\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkMarshal(spec, s3.TypeV1, "{\"type\":\"s3/v1\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkMarshal(spec, s3.TypeV2, "{\"type\":\"s3/v2\",\"region\":\"region\",\"bucketName\":\"bucket\",\"objectKey\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkMarshal(spec, s3.LegacyType, "{\"type\":\"S3\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkMarshal(spec, s3.LegacyTypeV1, "{\"type\":\"S3/v1\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ })
+
+ It("deserializes versioned", func() {
+ checkDecode(spec, s3.TypeV1, "{\"type\":\"s3/v1\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkDecode(spec, s3.TypeV2, "{\"type\":\"s3/v2\",\"region\":\"region\",\"bucketName\":\"bucket\",\"objectKey\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+
+ checkDecode(spec, s3.LegacyTypeV1, "{\"type\":\"S3/v1\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkDecode(spec, s3.LegacyTypeV2, "{\"type\":\"S3/v2\",\"region\":\"region\",\"bucketName\":\"bucket\",\"objectKey\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ })
+
+ It("deserializes anonymous", func() {
+ checkDecode(spec, s3.Type, "{\"type\":\"s3\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkDecode(spec, s3.Type, "{\"type\":\"s3\",\"region\":\"region\",\"bucketName\":\"bucket\",\"objectKey\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+
+ checkDecode(spec, s3.LegacyType, "{\"type\":\"S3\",\"region\":\"region\",\"bucket\":\"bucket\",\"key\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ checkDecode(spec, s3.LegacyType, "{\"type\":\"S3\",\"region\":\"region\",\"bucketName\":\"bucket\",\"objectKey\":\"key\",\"version\":\"version\",\"mediaType\":\"tar/gz\"}")
+ })
+ })
+
+ Context("accessmethod", func() {
+ var (
+ env *Builder
+ accessSpec *s3.AccessSpec
+ downloader downloader.Downloader
+ expectedContent []byte
+ err error
+ mcc ocm.Context
+ fs vfs.FileSystem
+ ctx datacontext.Context
+ )
+
+ BeforeEach(func() {
+ expectedContent, err = os.ReadFile(filepath.Join("testdata", "repo.tar.gz"))
+ Expect(err).ToNot(HaveOccurred())
+ env = NewBuilder()
+ downloader = &mockDownloader{
+ expected: expectedContent,
+ }
+ accessSpec = s3.New(
+ "region",
+ "bucket",
+ "key",
+ "version",
+ "tar/gz",
+ downloader,
+ )
+ fs, err = osfs.NewTempFileSystem()
+ Expect(err).To(Succeed())
+ ctx = datacontext.New(nil)
+ vfsattr.Set(ctx, fs)
+ tmpcache.Set(ctx, &tmpcache.Attribute{Path: "/tmp", Filesystem: fs})
+ mcc = ocm.New(datacontext.MODE_INITIAL)
+ mcc.CredentialsContext().SetCredentialsForConsumer(credentials.ConsumerIdentity{credentials.ID_TYPE: identity.CONSUMER_TYPE}, credentials.DirectCredentials{
+ "accessKeyID": "accessKeyID",
+ "accessSecret": "accessSecret",
+ })
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ vfs.Cleanup(fs)
+ })
+
+ It("provides comsumer id", func() {
+ m, err := accessSpec.AccessMethod(&cpi.DummyComponentVersionAccess{Context: env.OCMContext()})
+ Expect(err).ToNot(HaveOccurred())
+ Expect(credentials.GetProvidedConsumerId(m)).To(Equal(credentials.NewConsumerIdentity(identity.CONSUMER_TYPE,
+ identity.ID_PATHPREFIX, "bucket/key/version")))
+ })
+
+ It("downloads s3 objects", func() {
+ m, err := accessSpec.AccessMethod(&mockComponentVersionAccess{context: mcc})
+ Expect(err).ToNot(HaveOccurred())
+ defer Close(m, "method")
+ blob, err := m.Get()
+ Expect(err).ToNot(HaveOccurred())
+ Expect(blob).To(Equal(expectedContent))
+ })
+
+ When("the downloader fails to download the bucket object", func() {
+ BeforeEach(func() {
+ downloader = &mockDownloader{
+ err: fmt.Errorf("object not found"),
+ }
+ accessSpec = s3.New(
+ "region",
+ "bucket",
+ "key",
+ "version",
+ "tar/gz",
+ downloader,
+ )
+ })
+ It("errors", func() {
+ m, err := accessSpec.AccessMethod(&mockComponentVersionAccess{context: mcc})
+ Expect(err).ToNot(HaveOccurred())
+ _, err = m.Get()
+ Expect(err).To(MatchError(ContainSubstring("object not found")))
+ })
+ })
+ })
+})
+
+type mockComponentVersionAccess struct {
+ ocm.ComponentVersionAccess
+ context ocm.Context
+}
+
+func (m *mockComponentVersionAccess) GetContext() ocm.Context {
+ return m.context
+}
diff --git a/pkg/contexts/ocm/accessmethods/s3/suite_test.go b/api/ocm/extensions/accessmethods/s3/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/s3/suite_test.go
rename to api/ocm/extensions/accessmethods/s3/suite_test.go
diff --git a/pkg/contexts/ocm/accessmethods/s3/testdata/repo.tar.gz b/api/ocm/extensions/accessmethods/s3/testdata/repo.tar.gz
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/s3/testdata/repo.tar.gz
rename to api/ocm/extensions/accessmethods/s3/testdata/repo.tar.gz
diff --git a/pkg/contexts/ocm/accessmethods/s3/v1.go b/api/ocm/extensions/accessmethods/s3/v1.go
similarity index 93%
rename from pkg/contexts/ocm/accessmethods/s3/v1.go
rename to api/ocm/extensions/accessmethods/s3/v1.go
index 174c9a520..5fae5f556 100644
--- a/pkg/contexts/ocm/accessmethods/s3/v1.go
+++ b/api/ocm/extensions/accessmethods/s3/v1.go
@@ -3,9 +3,9 @@ package s3
import (
. "github.com/mandelsoft/goutils/exception"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/runtime"
)
const TypeV1 = Type + runtime.VersionSeparator + "v1"
diff --git a/pkg/contexts/ocm/accessmethods/s3/v2.go b/api/ocm/extensions/accessmethods/s3/v2.go
similarity index 93%
rename from pkg/contexts/ocm/accessmethods/s3/v2.go
rename to api/ocm/extensions/accessmethods/s3/v2.go
index 84a377d68..fa13cfb7e 100644
--- a/pkg/contexts/ocm/accessmethods/s3/v2.go
+++ b/api/ocm/extensions/accessmethods/s3/v2.go
@@ -3,9 +3,9 @@ package s3
import (
. "github.com/mandelsoft/goutils/exception"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/runtime"
)
const TypeV2 = Type + runtime.VersionSeparator + "v2"
diff --git a/api/ocm/extensions/accessmethods/wget/cli.go b/api/ocm/extensions/accessmethods/wget/cli.go
new file mode 100644
index 000000000..3badac96a
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/wget/cli.go
@@ -0,0 +1,71 @@
+package wget
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.URLOption,
+ options.MediatypeOption,
+ options.HTTPHeaderOption,
+ options.HTTPVerbOption,
+ options.HTTPBodyOption,
+ options.HTTPRedirectOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.URLOption, config, "url")
+ flagsets.AddFieldByOptionP(opts, options.MediatypeOption, config, "mediaType")
+ flagsets.AddFieldByOptionP(opts, options.HTTPHeaderOption, config, "header")
+ flagsets.AddFieldByOptionP(opts, options.HTTPVerbOption, config, "verb")
+ flagsets.AddFieldByOptionP(opts, options.HTTPBodyOption, config, "body")
+ flagsets.AddFieldByOptionP(opts, options.HTTPRedirectOption, config, "noredirect")
+ return nil
+}
+
+var usage = `
+This method implements access to resources stored on an http server.
+`
+
+var formatV1 = `
+The url
is the url pointing to the http endpoint from which a resource is
+downloaded. The mimeType
can be used to specify the MIME type of the
+resource.
+
+This blob type specification supports the following fields:
+- **url
** *string*
+
+This REQUIRED property describes the url from which the resource is to be
+downloaded.
+
+- **mediaType
** *string*
+
+This OPTIONAL property describes the media type of the resource to be
+downloaded. If omitted, ocm tries to read the mediaType from the Content-Type header
+of the http response. If the mediaType cannot be set from the Content-Type header as well,
+ocm tries to deduct the mediaType from the URL. If that is not possible either, the default
+media type is defaulted to ` + mime.MIME_OCTET + `.
+
+- **header
** *map[string][]string*
+
+This OPTIONAL property describes the http headers to be set in the http request to the server.
+
+- **verb
** *string*
+
+This OPTIONAL property describes the http verb (also known as http request method) for the http
+request. If omitted, the http verb is defaulted to GET.
+
+- **body
** *[]byte*
+
+This OPTIONAL property describes the http body to be included in the request.
+
+- **noredirect
** *bool*
+
+This OPTIONAL property describes whether http redirects should be disabled. If omitted,
+it is defaulted to false (so, per default, redirects are enabled).
+`
diff --git a/api/ocm/extensions/accessmethods/wget/logging.go b/api/ocm/extensions/accessmethods/wget/logging.go
new file mode 100644
index 000000000..d78ec6a41
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/wget/logging.go
@@ -0,0 +1,18 @@
+package wget
+
+import (
+ "github.com/mandelsoft/logging"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var REALM = ocmlog.DefineSubRealm("access method for wget", "accessmethod/wget")
+
+type ContextProvider interface {
+ GetContext() cpi.Context
+}
+
+func Logger(c ContextProvider, keyValuePairs ...interface{}) logging.Logger {
+ return c.GetContext().Logger(REALM).WithValues(keyValuePairs...)
+}
diff --git a/api/ocm/extensions/accessmethods/wget/method.go b/api/ocm/extensions/accessmethods/wget/method.go
new file mode 100644
index 000000000..c30b1992b
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/wget/method.go
@@ -0,0 +1,176 @@
+package wget
+
+import (
+ "fmt"
+ "io"
+ "sync"
+
+ "github.com/mandelsoft/goutils/optionutils"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/wget/identity"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/blobaccess/wget"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type for a blob on an http server .
+const (
+ Type = "wget"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](Type, accspeccpi.WithDescription(usage)))
+ accspeccpi.RegisterAccessType(accspeccpi.NewAccessSpecType[*AccessSpec](TypeV1, accspeccpi.WithFormatSpec(formatV1), accspeccpi.WithConfigHandler(ConfigHandler())))
+}
+
+func Is(spec accspeccpi.AccessSpec) bool {
+ return spec != nil && spec.GetKind() == Type
+}
+
+// New creates a new WGET accessor for http resources.
+func New(url string, opts ...Option) *AccessSpec {
+ eff := optionutils.EvalOptions(opts...)
+
+ return &AccessSpec{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ URL: url,
+ MediaType: eff.MimeType,
+ Header: eff.Header,
+ Verb: eff.Verb,
+ Body: eff.Body,
+ NoRedirect: optionutils.AsValue(eff.NoRedirect),
+ }
+}
+
+// AccessSpec describes the access for files on HTTP and HTTPS servers.
+type AccessSpec struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // URLs to the files on a server
+ URL string `json:"URL"`
+ // MediaType is the media type of the object represented by the blob
+ MediaType string `json:"mediaType"`
+ // Header to be passed in the http request
+ Header map[string][]string `json:"header"`
+ // Verb is the http verb to be used for the request
+ Verb string `json:"verb"`
+ // Body is the body to be included in the http request
+ Body io.Reader `json:"body"`
+ // NoRedirect allows to disable redirects
+ NoRedirect bool `json:"noRedirect"`
+}
+
+var _ accspeccpi.AccessSpec = (*AccessSpec)(nil)
+
+func (a *AccessSpec) Describe(ctx accspeccpi.Context) string {
+ return fmt.Sprintf("Files from %s", a.URL)
+}
+
+func (a *AccessSpec) IsLocal(ctx accspeccpi.Context) bool {
+ return false
+}
+
+func (a *AccessSpec) GlobalAccessSpec(ctx accspeccpi.Context) accspeccpi.AccessSpec {
+ return a
+}
+
+func (a *AccessSpec) AccessMethod(access accspeccpi.ComponentVersionAccess) (accspeccpi.AccessMethod, error) {
+ return accspeccpi.AccessMethodForImplementation(&accessMethod{comp: access, spec: a}, nil)
+}
+
+///////////////////
+
+func (a *AccessSpec) GetURL() string {
+ return a.URL
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type accessMethod struct {
+ lock sync.Mutex
+ blob blobaccess.BlobAccess
+ comp accspeccpi.ComponentVersionAccess
+ spec *AccessSpec
+}
+
+var _ accspeccpi.AccessMethodImpl = (*accessMethod)(nil)
+
+func (_ *accessMethod) IsLocal() bool {
+ return false
+}
+
+func (m *accessMethod) GetKind() string {
+ return Type
+}
+
+func (m *accessMethod) AccessSpec() accspeccpi.AccessSpec {
+ return m.spec
+}
+
+func (m *accessMethod) Get() ([]byte, error) {
+ return blobaccess.BlobData(m.getBlob())
+}
+
+func (m *accessMethod) Reader() (io.ReadCloser, error) {
+ return blobaccess.BlobReader(m.getBlob())
+}
+
+func (m *accessMethod) MimeType() string {
+ if m.spec.MediaType != "" {
+ return m.spec.MediaType
+ }
+ blob, err := m.getBlob()
+ if err != nil {
+ return mime.MIME_OCTET
+ }
+ return blob.MimeType()
+}
+
+func (m *accessMethod) getBlob() (blobaccess.BlobAccess, error) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ if m.blob != nil {
+ return m.blob, nil
+ }
+
+ blob, err := wget.BlobAccess(m.spec.URL,
+ wget.WithMimeType(m.spec.MediaType),
+ wget.WithCredentialContext(m.comp.GetContext()),
+ wget.WithLoggingContext(m.comp.GetContext()),
+ wget.WithHeader(m.spec.Header),
+ wget.WithVerb(m.spec.Verb),
+ wget.WithBody(m.spec.Body),
+ wget.WithNoRedirect(m.spec.NoRedirect))
+ if err != nil {
+ return nil, err
+ }
+
+ m.blob = blob
+ return m.blob, nil
+}
+
+func (m *accessMethod) Close() error {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ var err error
+ if m.blob != nil {
+ err = m.blob.Close()
+ m.blob = nil
+ }
+
+ return err
+}
+
+func (m *accessMethod) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity {
+ return identity.GetConsumerId(m.spec.URL)
+}
+
+func (m *accessMethod) GetIdentityMatcher() string {
+ return identity.CONSUMER_TYPE
+}
diff --git a/api/ocm/extensions/accessmethods/wget/method_test.go b/api/ocm/extensions/accessmethods/wget/method_test.go
new file mode 100644
index 000000000..282c5ba8d
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/wget/method_test.go
@@ -0,0 +1,409 @@
+package wget_test
+
+import (
+ "bytes"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/ocm/extensions/accessmethods/wget"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/builtin/wget/identity"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/tech/signing/handlers/rsa"
+ "ocm.software/ocm/api/tech/signing/signutils"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+var (
+ caCert *x509.Certificate
+ caPriv *rsa.PrivateKey
+ caPub *rsa.PublicKey
+ caPEM []byte
+
+ serverCert *x509.Certificate
+ serverPriv *rsa.PrivateKey
+ serverPub *rsa.PublicKey
+ serverPEM []byte
+
+ httpsServerClientAuth http.Server
+ httpsServer http.Server
+ httpServer http.Server
+)
+
+const (
+ HTTP_PORT = ":18080"
+ HTTPS_PORT = ":1443"
+ HTTPS_PORT_WITH_CLIENT_AUTH = ":2443"
+
+ HTTP_HOST = "http://localhost" + HTTP_PORT
+ HTTPS_HOST = "https://localhost" + HTTPS_PORT
+ HTTPS_HOST_WITH_CLIENT_AUTH = "https://localhost" + HTTPS_PORT_WITH_CLIENT_AUTH
+
+ TO_MEMORY = "/tomemory"
+ TO_FILE = "/tofile"
+ BASIC_LOGIN = "/basic-login"
+ BEARER_LOGIN = "/bearer-login"
+ ECHO_HEADERS = "/headers"
+ ECHO_BODY = "/body"
+ ECHO_METHOD = "/method"
+ CONTENT_TYPE = " /content-type"
+ DOT_EXT = "/somefile.tar"
+ REDIRECT = "/redirect"
+
+ USERNAME = "user"
+ PASSWORD = "password"
+ TOKEN = "token"
+
+ CONTENT = "hello world"
+ NOREDIRECT_CONTENT = "noredirect"
+)
+
+var _ = BeforeSuite(func() {
+ // setup certificate authority
+ _capriv, _capub := Must2(rsa.Handler{}.CreateKeyPair())
+ caPriv = _capriv.(*rsa.PrivateKey)
+ caPub = _capub.(*rsa.PublicKey)
+
+ caSpec := &signutils.Specification{
+ Subject: *signutils.CommonName("caCert-authority"),
+ Validity: 10 * time.Minute,
+ CAPrivateKey: caPriv,
+ IsCA: true,
+ Usages: []interface{}{x509.KeyUsageDigitalSignature},
+ }
+
+ caCert, caPEM = Must2(signutils.CreateCertificate(caSpec))
+
+ // use certificate authority to create httpsServer certificate
+ _serverPriv, _serverPub := Must2(rsa.Handler{}.CreateKeyPair())
+ serverPriv = _serverPriv.(*rsa.PrivateKey)
+ serverPub = _serverPub.(*rsa.PublicKey)
+
+ serverSpec := &signutils.Specification{
+ IsCA: false,
+ Subject: pkix.Name{CommonName: "localhost"},
+ Validity: 10 * time.Minute,
+ RootCAs: caCert,
+ CAChain: caCert,
+ CAPrivateKey: caPriv,
+ PublicKey: serverPub,
+ Usages: []interface{}{x509.ExtKeyUsageServerAuth},
+ Hosts: []string{"localhost", "127.0.0.1"},
+ }
+
+ serverCert, serverPEM = Must2(signutils.CreateCertificate(serverSpec))
+
+ // setup tls configuration for the httpsServer for https with the corresponding certs and keys
+ serverPrivPEM := Must(rsa.KeyData(serverPriv))
+ serverTlsCert := Must(tls.X509KeyPair(serverPEM, serverPrivPEM))
+
+ // ca's used by the server to validate client certificates
+ clientCaCertPool := x509.NewCertPool()
+ clientCaCertPool.AddCert(caCert)
+
+ tlsConfig := &tls.Config{
+ Certificates: []tls.Certificate{serverTlsCert},
+ }
+
+ tlsConfigClientAuth := &tls.Config{
+ Certificates: []tls.Certificate{serverTlsCert},
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ ClientCAs: clientCaCertPool,
+ }
+
+ // configure test routes
+ mux := http.NewServeMux()
+ mux.HandleFunc(TO_MEMORY, func(writer http.ResponseWriter, request *http.Request) {
+ n, err := writer.Write([]byte(CONTENT))
+ _, _ = n, err
+ })
+ mux.HandleFunc(BASIC_LOGIN, func(writer http.ResponseWriter, request *http.Request) {
+ username, password, ok := request.BasicAuth()
+ if !ok {
+ n, err := writer.Write([]byte(`failure`))
+ _, _ = n, err
+ }
+ if username != "" && password != "" {
+ res := fmt.Sprintf("%s:%s", username, password)
+ n, err := writer.Write([]byte(res))
+ _, _ = n, err
+ } else {
+ n, err := writer.Write([]byte(`failure`))
+ _, _ = n, err
+ }
+ })
+ mux.HandleFunc(BEARER_LOGIN, func(writer http.ResponseWriter, request *http.Request) {
+ auth := request.Header.Get("Authorization")
+ if auth == "" {
+ n, err := writer.Write([]byte(`failure`))
+ _, _ = n, err
+ } else {
+ bearer, ok := strings.CutPrefix(auth, "Bearer ")
+ if !ok {
+ n, err := writer.Write([]byte(`failure`))
+ _, _ = n, err
+ } else {
+ n, err := writer.Write([]byte(bearer))
+ _, _ = n, err
+ }
+ }
+ })
+ mux.HandleFunc(ECHO_HEADERS, func(writer http.ResponseWriter, request *http.Request) {
+ err := request.Header.Write(writer)
+ _ = err
+ })
+ mux.HandleFunc(ECHO_BODY, func(writer http.ResponseWriter, request *http.Request) {
+ b, err := io.ReadAll(request.Body)
+ _, err = writer.Write(b)
+ _ = err
+ })
+ mux.HandleFunc(ECHO_METHOD, func(writer http.ResponseWriter, request *http.Request) {
+ _, err := writer.Write([]byte(request.Method))
+ _ = err
+ })
+ mux.HandleFunc(CONTENT_TYPE, func(writer http.ResponseWriter, request *http.Request) {
+ writer.Header().Set("Content-Type", mime.MIME_TEXT)
+ })
+ mux.HandleFunc(DOT_EXT, func(writer http.ResponseWriter, request *http.Request) {})
+ mux.HandleFunc(REDIRECT, func(writer http.ResponseWriter, request *http.Request) {
+ writer.Header().Set("Location", TO_MEMORY)
+ writer.WriteHeader(307)
+ writer.Write([]byte(NOREDIRECT_CONTENT))
+ })
+
+ // setup an https and an http httpsServer
+ httpsServerClientAuth := &http.Server{
+ Addr: HTTPS_PORT_WITH_CLIENT_AUTH,
+ TLSConfig: tlsConfigClientAuth,
+ Handler: mux,
+ }
+
+ httpsServer := &http.Server{
+ Addr: HTTPS_PORT,
+ TLSConfig: tlsConfig,
+ Handler: mux,
+ }
+
+ httpServer := &http.Server{
+ Addr: HTTP_PORT,
+ Handler: mux,
+ }
+
+ go func() {
+ MustBeSuccessful(httpsServerClientAuth.ListenAndServeTLS("", ""))
+ }()
+
+ go func() {
+ MustBeSuccessful(httpsServer.ListenAndServeTLS("", ""))
+ }()
+
+ go func() {
+ MustBeSuccessful(httpServer.ListenAndServe())
+ }()
+})
+
+var _ = AfterSuite(func() {
+ MustBeSuccessful(httpsServerClientAuth.Close())
+ MustBeSuccessful(httpsServer.Close())
+ MustBeSuccessful(httpServer.Close())
+})
+
+var _ = Describe("wget access method", func() {
+ It("access content on http server", func() {
+ url := HTTP_HOST + TO_MEMORY
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+ Expect(string(b)).To(Equal(CONTENT))
+ })
+
+ It("access content on https server", func() {
+ url := HTTPS_HOST + TO_MEMORY
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ ctx.CredentialsContext().SetCredentialsForConsumer(identity.GetConsumerId(url), credentials.DirectCredentials{
+ identity.ATTR_CERTIFICATE_AUTHORITY: string(caPEM),
+ })
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+ Expect(string(b)).To(Equal(CONTENT))
+ })
+
+ It("access content on https server with client authentication", func() {
+ // create a client certificate
+ _clientPriv, _clientPub := Must2(rsa.Handler{}.CreateKeyPair())
+ clientPriv := _clientPriv.(*rsa.PrivateKey)
+ clientPrivData := Must(rsa.KeyData(clientPriv))
+ clientPub := _clientPub.(*rsa.PublicKey)
+
+ clientSpec := &signutils.Specification{
+ IsCA: false,
+ Subject: pkix.Name{CommonName: "localhost"},
+ Validity: 10 * time.Minute,
+ RootCAs: caCert,
+ CAChain: caCert,
+ CAPrivateKey: caPriv,
+ PublicKey: clientPub,
+ Usages: []interface{}{x509.ExtKeyUsageClientAuth},
+ Hosts: []string{"localhost", "127.0.0.1"},
+ }
+
+ _, clientPEM := Must2(signutils.CreateCertificate(clientSpec))
+
+ // Request
+ url := HTTPS_HOST_WITH_CLIENT_AUTH + TO_MEMORY
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ ctx.CredentialsContext().SetCredentialsForConsumer(identity.GetConsumerId(url), credentials.DirectCredentials{
+ identity.ATTR_CERTIFICATE_AUTHORITY: string(caPEM),
+ identity.ATTR_CERTIFICATE: string(clientPEM),
+ identity.ATTR_PRIVATE_KEY: string(clientPrivData),
+ })
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+ Expect(string(b)).To(Equal(CONTENT))
+ })
+
+ It("check that username and password are passed correctly", func() {
+ url := HTTP_HOST + BASIC_LOGIN
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ ctx.CredentialsContext().SetCredentialsForConsumer(identity.GetConsumerId(url), credentials.DirectCredentials{
+ identity.ATTR_USERNAME: USERNAME,
+ identity.ATTR_PASSWORD: PASSWORD,
+ })
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+ Expect(string(b)).To(Equal(USERNAME + ":" + PASSWORD))
+ })
+
+ It("check that bearer token is passed correctly", func() {
+ url := HTTP_HOST + BEARER_LOGIN
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ ctx.CredentialsContext().SetCredentialsForConsumer(identity.GetConsumerId(url), credentials.DirectCredentials{
+ identity.ATTR_IDENTITY_TOKEN: TOKEN,
+ })
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+ Expect(string(b)).To(Equal(TOKEN))
+ })
+
+ It("check that basic auth is merged correctly with other provided headers", func() {
+ url := HTTP_HOST + ECHO_HEADERS
+ headers := map[string][]string{"Content-Type": {"text/plain"}}
+ spec := New(url, WithHeader(headers))
+
+ ctx := ocm.DefaultContext()
+ ctx.CredentialsContext().SetCredentialsForConsumer(identity.GetConsumerId(url), credentials.DirectCredentials{
+ identity.ATTR_USERNAME: USERNAME,
+ identity.ATTR_PASSWORD: PASSWORD,
+ })
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+
+ Expect(strings.Contains(string(b), "Content-Type: text/plain")).To(BeTrue())
+ Expect(strings.Contains(string(b), "Authorization: Basic")).To(BeTrue())
+ })
+
+ It("check detect mime type based on content-type response header", func() {
+ url := HTTP_HOST + ECHO_HEADERS
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ Expect(m.MimeType()).To(Equal(mime.MIME_TEXT))
+ })
+
+ It("check deduction of mime type based on url", func() {
+ url := HTTP_HOST + DOT_EXT
+ spec := New(url)
+
+ ctx := ocm.DefaultContext()
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ Expect(m.MimeType()).To(Equal("application/x-tar"))
+ })
+
+ It("check passing an http body", func() {
+ url := HTTP_HOST + ECHO_BODY
+
+ content := `hello world`
+ spec := New(url, WithBody(bytes.NewReader([]byte(content))))
+
+ ctx := ocm.DefaultContext()
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+
+ Expect(string(b)).To(Equal(content))
+ })
+
+ It("check passing an http verb", func() {
+ url := HTTP_HOST + ECHO_METHOD
+
+ method := http.MethodPost
+ spec := New(url, WithVerb(method))
+
+ ctx := ocm.DefaultContext()
+ m := Must(spec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(m, "method")
+
+ b := Must(m.Get())
+
+ Expect(string(b)).To(Equal(method))
+ })
+
+ It("check noredirect behavior", func() {
+ url := HTTP_HOST + REDIRECT
+
+ redirectSpec := New(url, WithNoRedirect(false))
+ noredirectSpec := New(url, WithNoRedirect(true))
+
+ ctx := ocm.DefaultContext()
+ redirectMethod := Must(redirectSpec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(redirectMethod, "redirectmethod")
+
+ noredirectMethod := Must(noredirectSpec.AccessMethod(&cpi.DummyComponentVersionAccess{ctx}))
+ defer Close(noredirectMethod, "noredirectmethod")
+
+ redirectContent := Must(redirectMethod.Get())
+ Expect(string(redirectContent)).To(Equal(CONTENT))
+
+ noredirectContent := Must(noredirectMethod.Get())
+ Expect(string(noredirectContent)).To(Equal(NOREDIRECT_CONTENT))
+ })
+})
diff --git a/api/ocm/extensions/accessmethods/wget/options.go b/api/ocm/extensions/accessmethods/wget/options.go
new file mode 100644
index 000000000..fd0cf46f7
--- /dev/null
+++ b/api/ocm/extensions/accessmethods/wget/options.go
@@ -0,0 +1,33 @@
+package wget
+
+import (
+ "io"
+ "net/http"
+
+ "ocm.software/ocm/api/utils/blobaccess/wget"
+)
+
+type (
+ Options = wget.Options
+ Option = wget.Option
+)
+
+func WithMimeType(mime string) Option {
+ return wget.WithMimeType(mime)
+}
+
+func WithHeader(h http.Header) Option {
+ return wget.WithHeader(h)
+}
+
+func WithVerb(v string) Option {
+ return wget.WithVerb(v)
+}
+
+func WithBody(v io.Reader) Option {
+ return wget.WithBody(v)
+}
+
+func WithNoRedirect(r ...bool) Option {
+ return wget.WithNoRedirect(r...)
+}
diff --git a/pkg/contexts/ocm/accessmethods/wget/suite_test.go b/api/ocm/extensions/accessmethods/wget/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/accessmethods/wget/suite_test.go
rename to api/ocm/extensions/accessmethods/wget/suite_test.go
diff --git a/api/ocm/extensions/actionhandler/init.go b/api/ocm/extensions/actionhandler/init.go
new file mode 100644
index 000000000..cc3c41922
--- /dev/null
+++ b/api/ocm/extensions/actionhandler/init.go
@@ -0,0 +1,5 @@
+package actionhandler
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/actionhandler/plugin"
+)
diff --git a/api/ocm/extensions/actionhandler/plugin/action_test.go b/api/ocm/extensions/actionhandler/plugin/action_test.go
new file mode 100644
index 000000000..f9158954f
--- /dev/null
+++ b/api/ocm/extensions/actionhandler/plugin/action_test.go
@@ -0,0 +1,87 @@
+//go:build unix
+
+package plugin_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
+
+ "ocm.software/ocm/api/datacontext/action/handlers"
+ oci_repository_prepare "ocm.software/ocm/api/oci/extensions/actions/oci-repository-prepare"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/actionhandler/plugin"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+)
+
+const PLUGIN = "test"
+
+var _ = Describe("plugin action handler", func() {
+ var ctx ocm.Context
+ var registry plugins.Set
+ var env *Builder
+ var plugins TempPluginDir
+
+ BeforeEach(func() {
+ env = NewBuilder(nil)
+ ctx = env.OCMContext()
+ plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata"))
+ p := registry.Get("action")
+ Expect(p).NotTo(BeNil())
+ })
+
+ AfterEach(func() {
+ plugins.Cleanup()
+ env.Cleanup()
+ })
+
+ It("executes with no plugin registration", func() {
+ result := Must(oci_repository_prepare.Execute(ctx.GetActions(), "ghcr.io", "mandelsoft", nil))
+ Expect(result).To(BeNil())
+ })
+
+ It("executes with no handler", func() {
+ registration.RegisterExtensions(ctx)
+ result := Must(oci_repository_prepare.Execute(ctx.GetActions(), "mandelsoft.org", "mandelsoft", nil))
+ Expect(result).To(BeNil())
+ })
+
+ It("used default registration", func() {
+ registration.RegisterExtensions(ctx)
+ opts := handlers.NewOptions(handlers.ForAction(oci_repository_prepare.Type), handlers.ForAction("test"), handlers.ForSelectors("mandelsoft.org"))
+ MustFailWithMessage(plugin.RegisterActionHandler(ctx.AttributesContext(), "action", ctx, opts), "action \"test\" is unknown for plugin action")
+ })
+
+ It("uses default registration", func() {
+ registration.RegisterExtensions(ctx)
+ result := Must(oci_repository_prepare.Execute(ctx.GetActions(), "ghcr.io", "mandelsoft", nil))
+ Expect(result).NotTo(BeNil())
+ Expect(result.Message).To(Equal("all good"))
+ })
+
+ It("uses default pattern registration", func() {
+ registration.RegisterExtensions(ctx)
+ result := Must(oci_repository_prepare.Execute(ctx.GetActions(), "xyz.dkr.ecr.us-west-2.amazonaws.com", "mandelsoft", nil))
+ Expect(result).NotTo(BeNil())
+ Expect(result.Message).To(Equal("all good"))
+ })
+
+ It("executes action for dynamic registration", func() {
+ registration.RegisterExtensions(ctx)
+ opts := handlers.NewOptions(handlers.ForAction(oci_repository_prepare.Type), handlers.ForAction(oci_repository_prepare.Type), handlers.ForSelectors("mandelsoft.org"))
+ MustBeSuccessful(plugin.RegisterActionHandler(ctx.AttributesContext(), "action", ctx, opts))
+
+ result := Must(oci_repository_prepare.Execute(ctx.GetActions(), "mandelsoft.org", "mandelsoft", nil))
+ Expect(result.Message).To(Equal("all good"))
+ })
+
+ It("executed action after abstract registration", func() {
+ registration.RegisterExtensions(ctx)
+ opts := handlers.NewOptions(handlers.ForAction(oci_repository_prepare.Type), handlers.ForAction(oci_repository_prepare.Type), handlers.ForSelectors("mandelsoft.org"))
+ ok := Must(ctx.GetActions().RegisterByName("plugin/action", ctx.OCIContext(), ctx, opts))
+ Expect(ok).To(BeTrue())
+ })
+})
diff --git a/pkg/contexts/ocm/actionhandler/plugin/actionhandler.go b/api/ocm/extensions/actionhandler/plugin/actionhandler.go
similarity index 76%
rename from pkg/contexts/ocm/actionhandler/plugin/actionhandler.go
rename to api/ocm/extensions/actionhandler/plugin/actionhandler.go
index 453137c5e..b2127bcfa 100644
--- a/pkg/contexts/ocm/actionhandler/plugin/actionhandler.go
+++ b/api/ocm/extensions/actionhandler/plugin/actionhandler.go
@@ -5,10 +5,10 @@ import (
"github.com/mandelsoft/goutils/errors"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/contexts/datacontext/action"
- "github.com/open-component-model/ocm/pkg/contexts/datacontext/action/handlers"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin"
+ "ocm.software/ocm/api/datacontext/action"
+ "ocm.software/ocm/api/datacontext/action/handlers"
+ "ocm.software/ocm/api/ocm/plugin"
+ common "ocm.software/ocm/api/utils/misc"
)
// pluginHandler delegates action to a plugin based handler.
diff --git a/api/ocm/extensions/actionhandler/plugin/registration.go b/api/ocm/extensions/actionhandler/plugin/registration.go
new file mode 100644
index 000000000..8aa4b9728
--- /dev/null
+++ b/api/ocm/extensions/actionhandler/plugin/registration.go
@@ -0,0 +1,92 @@
+package plugin
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/datacontext/action/handlers"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+func init() {
+ handlers.DefaultRegistry().RegisterRegistrationHandler("plugin", &RegistrationHandler{})
+}
+
+type RegistrationHandler struct{}
+
+var _ handlers.HandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, target handlers.Target, config handlers.HandlerConfig, olist ...handlers.Option) (bool, error) {
+ path := cpi.NewNamePath(handler)
+
+ if config == nil {
+ return true, fmt.Errorf("config required")
+ }
+
+ ctx, ok := config.(cpi.Context)
+ if !ok {
+ return true, fmt.Errorf("expected ocm.Context as config but found: %T", config)
+ }
+ if len(path) != 1 {
+ return true, fmt.Errorf("plugin handler must be of the form ` + ConfigType + `
can be used to define
+the default hash algorithm used to calculate digests for resources.
+It supports the field hashAlgorithm
, with one of the following
+values:
+` + listformat.FormatList(sha256.Algorithm, signing.DefaultRegistry().HasherNames()...)
diff --git a/pkg/contexts/ocm/attrs/hashattr/suite_test.go b/api/ocm/extensions/attrs/hashattr/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/attrs/hashattr/suite_test.go
rename to api/ocm/extensions/attrs/hashattr/suite_test.go
diff --git a/api/ocm/extensions/attrs/init.go b/api/ocm/extensions/attrs/init.go
new file mode 100644
index 000000000..283af4105
--- /dev/null
+++ b/api/ocm/extensions/attrs/init.go
@@ -0,0 +1,12 @@
+package attrs
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/compatattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/hashattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/mapocirepoattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/plugindirattr"
+ _ "ocm.software/ocm/api/ocm/extensions/attrs/signingattr"
+)
diff --git a/api/ocm/extensions/attrs/keepblobattr/attr.go b/api/ocm/extensions/attrs/keepblobattr/attr.go
new file mode 100644
index 000000000..78475b05b
--- /dev/null
+++ b/api/ocm/extensions/attrs/keepblobattr/attr.go
@@ -0,0 +1,61 @@
+package keepblobattr
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "github.com/mandelsoft/ocm/keeplocalblob"
+ ATTR_SHORT = "keeplocalblob"
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*bool*
+Keep local blobs when importing OCI artifacts to OCI registries from localBlob
+access methods. By default, they will be expanded to OCI artifacts with the
+access method ociRegistry
. If this option is set to true, they will be stored
+as local blobs, also. The access method will still be localBlob
but with a nested
+ociRegistry
access method for describing the global access.
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ if _, ok := v.(bool); !ok {
+ return nil, fmt.Errorf("boolean required")
+ }
+ return marshaller.Marshal(v)
+}
+
+func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) {
+ var value bool
+ err := unmarshaller.Unmarshal(data, &value)
+ return value, err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx datacontext.Context) bool {
+ a := ctx.GetAttributes().GetAttribute(ATTR_KEY)
+ if a == nil {
+ return false
+ }
+ return a.(bool)
+}
+
+func Set(ctx datacontext.Context, flag bool) error {
+ return ctx.GetAttributes().SetAttribute(ATTR_KEY, flag)
+}
diff --git a/api/ocm/extensions/attrs/keepblobattr/attr_test.go b/api/ocm/extensions/attrs/keepblobattr/attr_test.go
new file mode 100644
index 000000000..c67e83271
--- /dev/null
+++ b/api/ocm/extensions/attrs/keepblobattr/attr_test.go
@@ -0,0 +1,41 @@
+package keepblobattr_test
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/ocm"
+ me "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+var _ = Describe("attribute", func() {
+ var ctx ocm.Context
+ var cfgctx config.Context
+
+ BeforeEach(func() {
+ cfgctx = config.WithSharedAttributes(datacontext.New(nil)).New()
+ credctx := credentials.WithConfigs(cfgctx).New()
+ ocictx := oci.WithCredentials(credctx).New()
+ ctx = ocm.WithOCIRepositories(ocictx).New()
+ })
+ It("local setting", func() {
+ Expect(me.Get(ctx)).To(BeFalse())
+ Expect(me.Set(ctx, true)).To(Succeed())
+ Expect(me.Get(ctx)).To(BeTrue())
+ })
+
+ It("global setting", func() {
+ Expect(me.Get(cfgctx)).To(BeFalse())
+ Expect(me.Set(ctx, true)).To(Succeed())
+ Expect(me.Get(ctx)).To(BeTrue())
+ })
+
+ It("parses string", func() {
+ Expect(me.AttributeType{}.Decode([]byte("true"), runtime.DefaultJSONEncoding)).To(BeTrue())
+ })
+})
diff --git a/pkg/contexts/ocm/attrs/keepblobattr/suite_test.go b/api/ocm/extensions/attrs/keepblobattr/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/attrs/keepblobattr/suite_test.go
rename to api/ocm/extensions/attrs/keepblobattr/suite_test.go
diff --git a/api/ocm/extensions/attrs/mapocirepoattr/attr.go b/api/ocm/extensions/attrs/mapocirepoattr/attr.go
new file mode 100644
index 000000000..3f5c0ad53
--- /dev/null
+++ b/api/ocm/extensions/attrs/mapocirepoattr/attr.go
@@ -0,0 +1,249 @@
+package mapocirepoattr
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "golang.org/x/exp/maps"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/oci/grammar"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ATTR_KEY = "github.com/mandelsoft/ocm/mapocirepo"
+ ATTR_SHORT = "mapocirepo"
+)
+
+func init() {
+ datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT)
+}
+
+type AttributeType struct{}
+
+func (a AttributeType) Name() string {
+ return ATTR_KEY
+}
+
+func (a AttributeType) Description() string {
+ return `
+*bool|YAML*
+When uploading an OCI artifact blob to an OCI based OCM repository and the
+artifact is uploaded as OCI artifact, the repository path part is shortened,
+either by hashing all but the last repository name part or by executing
+some prefix based name mappings.
+
+If a boolean is given the short hash or none mode is enabled.
+The YAML flavor uses the following fields:
+- *mode
* *string*: hash
, shortHash
, prefixMapping
+ or none
. If unset, no mapping is done.
+- *prefixMappings
*: *map[string]string* repository path prefix mapping.
+- *prefix
*: *string* repository prefix to use (replaces potential sub path of OCM repo).
+ or none
.
+- *prefixMapping
*: *map[string]string* repository path prefix mapping.
+
+Notes:
+
+- The mapping only occurs in transfer commands and only when transferring to OCI registries (e.g.
+ when transferring to a CTF archive this option will be ignored).
+- The mapping only happens for local resources. When external image references are transferred (with
+ option --copy-resources) the mapping will be ignored.
+- The mapping in mode prefixMapping
requires a full prefix of the composed final name.
+ Partial matches are not supported. The host name of the target will be skipped.
+- The artifact name of the component-descriptor is not mapped.
+- If the mapping is provided on the command line it must be JSON format and needs to be properly
+ escaped (see example below).
+
+Example:
+
+Assume a component named github.com/my_org/myexamplewithalongname
and a chart name
+echo
in the Charts.yaml
of the chart archive. The following input to a
+resource.yaml
creates a component version:
+
++name: mychart +type: helmChart +input: + type: helm + path: charts/mychart.tgz +--- +name: myimage +type: ociImage +version: 0.1.0 +input: + type: ociImage + repository: ocm/ocm.software/ocmcli/ocmcli-image + path: ghcr.io/acme/ocm/ocm.software/ocmcli/ocmcli-image:0.1.0 ++ +The following command: + +
+ocm "-X mapocirepo={\"mode\":\"mapping\",\"prefixMappings\":{\"acme/github.com/my_org/myexamplewithalongname/ocm/ocm.software/ocmcli\":\"acme/cli\", \"acme/github.com/my_org/myexamplewithalongnameabc123\":\"acme/mychart\"}}" transfer ctf -f --copy-resources ./ctf ghcr.io/acme ++ +will result in the following artifacts in
ghcr.io/my_org
:
+
++mychart/echo +cli/ocmcli-image ++ +Note that the host name part of the transfer target
ghcr.io/acme
is excluded from the
+prefix but the path acme
is considered.
+
+The same using a config file .ocmconfig
:
++type: generic.config.ocm.software/v1 +configurations: +... +- type: attributes.config.ocm.software + attributes: + ... + mapocirepo: + mode: mapping + prefixMappings: + acme/github.com/my\_org/myexamplewithalongname/ocm/ocm.software/ocmcli: acme/cli + acme/github.com/my\_org/myexamplewithalongnameabc123: acme/mychart ++ +
+ocm transfer ca -f --copy-resources ./ca ghcr.io/acme ++` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if _, ok := v.(bool); ok { + return marshaller.Marshal(&Attribute{Mode: ShortHashMode}) + } + + if _, ok := v.(*Attribute); ok { + return marshaller.Marshal(v) + } + + return nil, fmt.Errorf("boolean or attribute struct required") +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + var value bool + attr := &Attribute{} + + err := unmarshaller.Unmarshal(data, attr) + if err == nil { + switch attr.Mode { + case "": + case NoneMode: + case HashMode: + case ShortHashMode: + case MappingMode: + default: + return nil, errors.ErrInvalid("mode", attr.Mode) + } + return attr, nil + } + + err = unmarshaller.Unmarshal(data, &value) + if err == nil { + if value { + attr.Mode = ShortHashMode + } else { + attr.Mode = NoneMode + } + attr.PrefixMappings = map[string]string{} + return attr, nil + } + + return value, err +} + +//////////////////////////////////////////////////////////////////////////////// + +const ( + NoneMode = "none" + HashMode = "hash" + ShortHashMode = "shortHash" + MappingMode = "mapping" +) + +type Attribute struct { + Mode string `json:"mode"` + Always bool `json:"always,omitempty"` + Prefix *string `json:"prefix,omitempty"` + PrefixMappings map[string]string `json:"prefixMappings,omitempty"` +} + +func (a *Attribute) Map(name string) string { + if len(a.PrefixMappings) == 0 { + a.PrefixMappings = map[string]string{} + } + switch a.Mode { + case "", NoneMode: + return name + case HashMode, ShortHashMode: + return a.Hash(name, a.Mode == ShortHashMode) + case MappingMode: + return a.MapPrefix(name) + } + return name +} + +func (a *Attribute) MapPrefix(name string) string { + keys := utils.StringMapKeys(a.PrefixMappings) + for i := range keys { + k := keys[len(keys)-i-1] + if strings.HasPrefix(name, k+grammar.RepositorySeparator) { + name = a.PrefixMappings[k] + name[len(k):] + break + } + } + return name +} + +func (a *Attribute) Hash(name string, short bool) string { + if idx := strings.LastIndex(name, grammar.RepositorySeparator); idx > 0 { + prefix := name[:idx] + sum := sha256.Sum256([]byte(prefix)) + n := hex.EncodeToString(sum[:]) + if short { + n = n[:8] + } + n += grammar.RepositorySeparator + name[idx+1:] + if a.Always || len(n) < len(name) { + name = n + } + } + return name +} + +func (a *Attribute) Copy() *Attribute { + n := *a + n.PrefixMappings = maps.Clone(n.PrefixMappings) + return &n +} + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctx datacontext.Context) *Attribute { + a := ctx.GetAttributes().GetAttribute(ATTR_KEY) + if a == nil { + return &Attribute{Mode: NoneMode} + } + if b, ok := a.(bool); ok { + if b { + return &Attribute{Mode: ShortHashMode} + } else { + return &Attribute{Mode: NoneMode} + } + } + return a.(*Attribute).Copy() +} + +func Set(ctx datacontext.Context, a *Attribute) error { + return ctx.GetAttributes().SetAttribute(ATTR_KEY, a) +} diff --git a/api/ocm/extensions/attrs/mapocirepoattr/attr_test.go b/api/ocm/extensions/attrs/mapocirepoattr/attr_test.go new file mode 100644 index 000000000..377f1ee8f --- /dev/null +++ b/api/ocm/extensions/attrs/mapocirepoattr/attr_test.go @@ -0,0 +1,35 @@ +package mapocirepoattr_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/datacontext" + me "ocm.software/ocm/api/ocm/extensions/attrs/mapocirepoattr" +) + +var _ = Describe("attribute", func() { + var ctx datacontext.Context + + BeforeEach(func() { + ctx = datacontext.New(nil) + }) + + It("set bool", func() { + Expect(me.Get(ctx)).To(Equal(&me.Attribute{Mode: me.NoneMode})) + ctx.GetAttributes().SetAttribute(me.ATTR_KEY, true) + a := me.Get(ctx) + Expect(a).To(Equal(&me.Attribute{Mode: me.ShortHashMode})) + hash := "5afa3f0f1b63d64422e7f93e2d9792b7c1f3b4462a931d80b25703f7e6fc79c2" + Expect(a.Map("very-long-path/with-many-path-segments/and-really-longer-than-a-hash/artifact")).To(Equal(hash[:8] + "/artifact")) + }) + + It("set attr", func() { + ctx.GetAttributes().SetAttribute(me.ATTR_KEY, &me.Attribute{Mode: me.MappingMode, PrefixMappings: map[string]string{"a": "b", "a/b": "c"}}) + a := me.Get(ctx) + Expect(a).To(Equal(&me.Attribute{Mode: me.MappingMode, PrefixMappings: map[string]string{"a": "b", "a/b": "c"}})) + Expect(a.Map("a/b/c")).To(Equal("c/c")) + Expect(a.Map("a/c")).To(Equal("b/c")) + Expect(a.Map("x/y")).To(Equal("x/y")) + }) +}) diff --git a/pkg/contexts/ocm/attrs/mapocirepoattr/suite_test.go b/api/ocm/extensions/attrs/mapocirepoattr/suite_test.go similarity index 100% rename from pkg/contexts/ocm/attrs/mapocirepoattr/suite_test.go rename to api/ocm/extensions/attrs/mapocirepoattr/suite_test.go diff --git a/api/ocm/extensions/attrs/ociuploadattr/attr.go b/api/ocm/extensions/attrs/ociuploadattr/attr.go new file mode 100644 index 000000000..eb3a07e31 --- /dev/null +++ b/api/ocm/extensions/attrs/ociuploadattr/attr.go @@ -0,0 +1,203 @@ +package ociuploadattr + +import ( + "bytes" + "fmt" + "strings" + "sync" + + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/oci" + ocicpi "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/ocm/ociuploadrepo" + ATTR_SHORT = "ociuploadrepo" +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*oci base repository ref* +Upload local OCI artifact blobs to a dedicated repository. +` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if _, ok := v.(*Attribute); !ok { + return nil, fmt.Errorf("OCI Upload Attribute structure required") + } + return marshaller.Marshal(v) +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + var value Attribute + err := unmarshaller.Unmarshal(data, &value) + if err == nil { + if value.Repository != nil { + if value.Repository.GetType() == "" { + return nil, errors.ErrInvalidWrap(errors.Newf("missing repository type"), oci.KIND_OCI_REFERENCE, string(data)) + } + return &value, nil + } + if value.Ref == "" { + return nil, errors.ErrInvalidWrap(errors.Newf("missing repository or ref"), oci.KIND_OCI_REFERENCE, string(data)) + } + data = []byte(value.Ref) + } + ref, err := oci.ParseRef(string(data)) + if err != nil { + return nil, errors.ErrInvalidWrap(err, oci.KIND_OCI_REFERENCE, string(data)) + } + if ref.Tag != nil || ref.Digest != nil { + return nil, errors.ErrInvalidWrap(err, oci.KIND_OCI_REFERENCE, string(data)) + } + return &Attribute{Ref: strings.Trim(string(data), "\"")}, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +type Attribute struct { + Ref string `json:"ociRef,omitempty"` + Repository *ocicpi.GenericRepositorySpec `json:"repository,omitempty"` + NamespacePrefix string `json:"namespacePrefix,omitempty"` + + lock sync.Mutex + ref *oci.RefSpec + spec []byte + + repo oci.Repository + prefix string +} + +func AttributeDescription() map[string]string { + return map[string]string{ + "ociRef": "an OCI repository reference", + "repository": "an OCI repository specification for the target OCI registry", + "namespacePrefix": "a namespace prefix used for the uploaded artifacts", + } +} + +func New(ref string) *Attribute { + return &Attribute{Ref: ref} +} + +func (a *Attribute) reset() { + a.repo = nil + a.prefix = "" + a.ref = nil + a.spec = nil +} + +func (a *Attribute) Close() error { + a.lock.Lock() + defer a.lock.Unlock() + if a.repo != nil { + defer a.reset() + return a.repo.Close() + } + return nil +} + +func (a *Attribute) GetInfo(ctx cpi.Context) (oci.Repository, *oci.UniformRepositorySpec, string, error) { + if a.Ref != "" { + return a.getByRef(ctx) + } + if a.Repository != nil { + return a.getBySpec(ctx) + } + return nil, nil, "", errors.ErrInvalid("ociuploadspec") +} + +func (a *Attribute) getBySpec(ctx cpi.Context) (oci.Repository, *oci.UniformRepositorySpec, string, error) { + data, err := a.Repository.MarshalJSON() + if err != nil { + return nil, nil, "", errors.Wrap(err, a.ref.String()) + } + + spec, err := a.Repository.Evaluate(ctx.OCIContext()) + if err != nil { + return nil, nil, "", errors.ErrInvalidWrap(err, oci.KIND_OCI_REFERENCE, string(data)) + } + + a.lock.Lock() + defer a.lock.Unlock() + + if a.spec == nil || bytes.Equal(a.spec, data) { + if a.repo != nil { + a.repo.Close() + a.reset() + } + + a.repo, err = ctx.OCIContext().RepositoryForSpec(spec) + if err != nil { + return nil, nil, "", err + } + + a.prefix = a.NamespacePrefix + a.spec = data + a.ref = &oci.RefSpec{UniformRepositorySpec: *spec.UniformRepositorySpec()} + ctx.Finalizer().Close(a) + } + return a.repo, &a.ref.UniformRepositorySpec, a.prefix, nil +} + +func (a *Attribute) getByRef(ctx cpi.Context) (oci.Repository, *oci.UniformRepositorySpec, string, error) { + ref, err := oci.ParseRef(a.Ref) + if err != nil { + return nil, nil, "", errors.ErrInvalidWrap(err, oci.KIND_OCI_REFERENCE, a.Ref) + } + if ref.Tag != nil || ref.Digest != nil { + return nil, nil, "", errors.ErrInvalidWrap(err, oci.KIND_OCI_REFERENCE, a.Ref) + } + + a.lock.Lock() + defer a.lock.Unlock() + if a.ref == nil || ref != *a.ref { + if a.repo != nil { + a.repo.Close() + a.reset() + } + + spec, err := ctx.OCIContext().MapUniformRepositorySpec(&ref.UniformRepositorySpec) + if err != nil { + return nil, nil, "", err + } + a.repo, err = ctx.OCIContext().RepositoryForSpec(spec) + if err != nil { + return nil, nil, "", err + } + a.prefix = ref.Repository + a.ref = &ref + ctx.Finalizer().Close(a) + } + return a.repo, &a.ref.UniformRepositorySpec, a.prefix, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctx datacontext.Context) *Attribute { + a := ctx.GetAttributes().GetAttribute(ATTR_KEY) + if a == nil { + return nil + } + return a.(*Attribute) +} + +func Set(ctx datacontext.Context, attr *Attribute) error { + return ctx.GetAttributes().SetAttribute(ATTR_KEY, attr) +} diff --git a/api/ocm/extensions/attrs/ociuploadattr/attr_test.go b/api/ocm/extensions/attrs/ociuploadattr/attr_test.go new file mode 100644 index 000000000..2761ec3ca --- /dev/null +++ b/api/ocm/extensions/attrs/ociuploadattr/attr_test.go @@ -0,0 +1,58 @@ +package ociuploadattr_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/extensions/repositories/ocireg" + "ocm.software/ocm/api/ocm" + me "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr" + "ocm.software/ocm/api/utils/runtime" +) + +var _ = Describe("attribute", func() { + var ctx ocm.Context + var cfgctx config.Context + + attr := &me.Attribute{Ref: "ref"} + + BeforeEach(func() { + cfgctx = config.WithSharedAttributes(datacontext.New(nil)).New() + credctx := credentials.WithConfigs(cfgctx).New() + ocictx := oci.WithCredentials(credctx).New() + ctx = ocm.WithOCIRepositories(ocictx).New() + }) + It("local setting", func() { + Expect(me.Get(ctx)).To(BeNil()) + Expect(me.Set(ctx, attr)).To(Succeed()) + Expect(me.Get(ctx)).To(BeIdenticalTo(attr)) + }) + + It("global setting", func() { + Expect(me.Get(cfgctx)).To(BeNil()) + Expect(me.Set(ctx, attr)).To(Succeed()) + Expect(me.Get(ctx)).To(BeIdenticalTo(attr)) + }) + + It("parses string", func() { + Expect(me.AttributeType{}.Decode([]byte("ref"), runtime.DefaultJSONEncoding)).To(Equal(&me.Attribute{Ref: "ref"})) + }) + + It("parses spec", func() { + spec, err := oci.ToGenericRepositorySpec(ocireg.NewRepositorySpec("ghcr.io")) + Expect(err).To(Succeed()) + attr := &me.Attribute{ + Repository: spec, + NamespacePrefix: "ref", + } + data, err := json.Marshal(attr) + Expect(err).To(Succeed()) + Expect(me.AttributeType{}.Decode(data, runtime.DefaultJSONEncoding)).To(Equal(attr)) + }) +}) diff --git a/pkg/contexts/ocm/attrs/ociuploadattr/suite_test.go b/api/ocm/extensions/attrs/ociuploadattr/suite_test.go similarity index 100% rename from pkg/contexts/ocm/attrs/ociuploadattr/suite_test.go rename to api/ocm/extensions/attrs/ociuploadattr/suite_test.go diff --git a/api/ocm/extensions/attrs/plugincacheattr/attr.go b/api/ocm/extensions/attrs/plugincacheattr/attr.go new file mode 100644 index 000000000..b737b14e5 --- /dev/null +++ b/api/ocm/extensions/attrs/plugincacheattr/attr.go @@ -0,0 +1,29 @@ +package plugincacheattr + +import ( + "ocm.software/ocm/api/datacontext" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/extensions/attrs/plugindirattr" + "ocm.software/ocm/api/ocm/plugin/cache" + "ocm.software/ocm/api/ocm/plugin/plugins" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/ocm/plugins" +) + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctxp ocm.ContextProvider) plugins.Set { + ctx := ctxp.OCMContext() + path := plugindirattr.Get(ctx) + + // avoid dead lock reading attribute during attribute creation + return ctx.GetAttributes().GetOrCreateAttribute(ATTR_KEY, func(ctx datacontext.Context) interface{} { + return plugins.New(ctx.(ocm.Context), path) + }).(plugins.Set) +} + +func Set(ctx ocm.Context, cache cache.PluginDir) error { + return ctx.GetAttributes().SetAttribute(ATTR_KEY, cache) +} diff --git a/api/ocm/extensions/attrs/plugindirattr/attr.go b/api/ocm/extensions/attrs/plugindirattr/attr.go new file mode 100644 index 000000000..2c23455a1 --- /dev/null +++ b/api/ocm/extensions/attrs/plugindirattr/attr.go @@ -0,0 +1,77 @@ +package plugindirattr + +import ( + "fmt" + "os" + + "github.com/mandelsoft/filepath/pkg/filepath" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/modern-go/reflect2" + + "ocm.software/ocm/api/datacontext" + utils "ocm.software/ocm/api/ocm/ocmutils" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/ocm/plugindir" + ATTR_SHORT = "plugindir" + + DEFAULT_PLUGIN_DIR = utils.DEFAULT_OCM_CONFIG_DIR + "/plugins" +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}, ATTR_SHORT) +} + +func DefaultDir(fs vfs.FileSystem) string { + home, _ := os.UserHomeDir() // use home if provided + if home != "" { + dir := filepath.Join(home, DEFAULT_PLUGIN_DIR) + if ok, err := vfs.DirExists(fs, dir); ok && err == nil { + return dir + } + } + return "" +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*plugin directory* +Directory to look for OCM plugin executables. +` +} + +func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) { + if _, ok := v.(string); !ok { + return nil, fmt.Errorf("directory path required") + } + return marshaller.Marshal(v) +} + +func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) { + var value string + err := unmarshaller.Unmarshal(data, &value) + return value, err +} + +//////////////////////////////////////////////////////////////////////////////// + +func Get(ctx datacontext.Context) string { + a := ctx.GetAttributes().GetAttribute(ATTR_KEY) + if reflect2.IsNil(a) { + return DefaultDir(osfs.New()) + } + return a.(string) +} + +func Set(ctx datacontext.Context, path string) error { + return ctx.GetAttributes().SetAttribute(ATTR_KEY, path) +} diff --git a/api/ocm/extensions/attrs/plugindirattr/attr_test.go b/api/ocm/extensions/attrs/plugindirattr/attr_test.go new file mode 100644 index 000000000..311f4b805 --- /dev/null +++ b/api/ocm/extensions/attrs/plugindirattr/attr_test.go @@ -0,0 +1,26 @@ +package plugindirattr_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "ocm.software/ocm/api/config" + "ocm.software/ocm/api/datacontext" + me "ocm.software/ocm/api/ocm/extensions/attrs/plugindirattr" +) + +var _ = Describe("attribute", func() { + var ctx config.Context + + attr := "___test___" + + BeforeEach(func() { + ctx = config.WithSharedAttributes(datacontext.New(nil)).New() + }) + + It("local setting", func() { + Expect(me.Get(ctx)).NotTo(Equal(attr)) + Expect(me.Set(ctx, attr)).To(Succeed()) + Expect(me.Get(ctx)).To(BeIdenticalTo(attr)) + }) +}) diff --git a/pkg/contexts/ocm/attrs/plugindirattr/suite_test.go b/api/ocm/extensions/attrs/plugindirattr/suite_test.go similarity index 100% rename from pkg/contexts/ocm/attrs/plugindirattr/suite_test.go rename to api/ocm/extensions/attrs/plugindirattr/suite_test.go diff --git a/api/ocm/extensions/attrs/signingattr/attr.go b/api/ocm/extensions/attrs/signingattr/attr.go new file mode 100644 index 000000000..94a609305 --- /dev/null +++ b/api/ocm/extensions/attrs/signingattr/attr.go @@ -0,0 +1,102 @@ +package signingattr + +import ( + "github.com/mandelsoft/goutils/errors" + + "ocm.software/ocm/api/datacontext" + ocm "ocm.software/ocm/api/ocm/types" + "ocm.software/ocm/api/tech/signing" + "ocm.software/ocm/api/utils/runtime" +) + +const ( + ATTR_KEY = "github.com/mandelsoft/ocm/signing" + ATTR_SHORT = "signing" +) + +type ( + Context = ocm.Context + ContextProvider = ocm.ContextProvider +) + +func init() { + datacontext.RegisterAttributeType(ATTR_KEY, AttributeType{}) +} + +type AttributeType struct{} + +func (a AttributeType) Name() string { + return ATTR_KEY +} + +func (a AttributeType) Description() string { + return ` +*JSON* +Public and private Key settings given as JSON document with the following +format: + +
+{ + "publicKeys"": [ + "<provider>": { + "data": ""<base64>" + } + ], + "privateKeys"": [ + "<provider>": { + "path": ""<file path>" + } + ] ++ +One of following data fields are possible: +-
data
: base64 encoded binary data
+- stringdata
: plain text data
+- path
: a file path to read the data from
+`
+}
+
+func (a AttributeType) Encode(v interface{}, marshaller runtime.Marshaler) ([]byte, error) {
+ if _, ok := v.(signing.Registry); ok {
+ return nil, nil
+ }
+ return nil, errors.ErrNotSupported("encoding of key registry")
+}
+
+func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (interface{}, error) {
+ var value Config
+ err := unmarshaller.Unmarshal(data, &value)
+ if err != nil {
+ return nil, err
+ }
+ value.SetType(ConfigType)
+ registry := signing.NewRegistry(signing.DefaultHandlerRegistry(), signing.DefaultKeyRegistry())
+ value.ApplyToRegistry(registry)
+ return registry, err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Get(ctx ContextProvider) signing.Registry {
+ a := ctx.OCMContext().GetAttributes().GetAttribute(ATTR_KEY)
+ if a == nil {
+ return signing.DefaultRegistry()
+ }
+ return a.(signing.Registry)
+}
+
+func SetKeyRegistry(ctx ContextProvider, registry signing.KeyRegistry) error {
+ old := Get(ctx)
+ r := signing.NewRegistry(old.HandlerRegistry(), registry)
+ return ctx.OCMContext().GetAttributes().SetAttribute(ATTR_KEY, r)
+}
+
+func SetHandlerRegistry(ctx ContextProvider, registry signing.HandlerRegistry) error {
+ old := Get(ctx)
+ r := signing.NewRegistry(registry, old.KeyRegistry())
+ return ctx.OCMContext().GetAttributes().SetAttribute(ATTR_KEY, r)
+}
+
+func Set(ctx ContextProvider, registry signing.Registry) error {
+ return ctx.OCMContext().GetAttributes().SetAttribute(ATTR_KEY, registry)
+}
diff --git a/api/ocm/extensions/attrs/signingattr/attr_test.go b/api/ocm/extensions/attrs/signingattr/attr_test.go
new file mode 100644
index 000000000..8088b777d
--- /dev/null
+++ b/api/ocm/extensions/attrs/signingattr/attr_test.go
@@ -0,0 +1,75 @@
+package signingattr_test
+
+import (
+ "encoding/json"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/config"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/attrs/signingattr"
+)
+
+const NAME = "test"
+
+var _ = Describe("attribute", func() {
+ var cfgctx config.Context
+ var ocmctx ocm.Context
+
+ BeforeEach(func() {
+ ocmctx = ocm.New(datacontext.MODE_EXTENDED)
+ cfgctx = ocmctx.ConfigContext()
+ })
+
+ It("marshal/unmarshal", func() {
+ cfg := signingattr.New()
+ cfg.AddPublicKeyData(NAME, []byte("keydata"))
+
+ data, err := json.Marshal(cfg)
+ Expect(err).To(Succeed())
+
+ r := &signingattr.Config{}
+ Expect(json.Unmarshal(data, r)).To(Succeed())
+ Expect(r).To(Equal(cfg))
+ })
+
+ It("applies public key", func() {
+ cfg := signingattr.New()
+ cfg.AddPublicKeyData(NAME, []byte("keydata"))
+
+ Expect(cfgctx.ApplyConfig(cfg, "from test")).To(Succeed())
+ Expect(signingattr.Get(ocmctx).GetPublicKey(NAME)).To(Equal([]byte("keydata")))
+ })
+
+ It("applies root certificate", func() {
+ certdata := `
+-----BEGIN CERTIFICATE-----
+MIIDBDCCAeygAwIBAgIQF+kRr0G+faDEAH5Y4P1J7DANBgkqhkiG9w0BAQsFADAc
+MQwwCgYDVQQKEwNPQ00xDDAKBgNVBAMTA29jbTAeFw0yMzEyMjkxMDIyMzdaFw0y
+NDEyMjgxMDIyMzdaMBwxDDAKBgNVBAoTA09DTTEMMAoGA1UEAxMDb2NtMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpTQIQFNy23ygef3pshdeNjT7TME
+kPEuqrqcF3KIX1cX16pHMQeU+VzXAFRj3xCy3LAM8ZzLsdHSwZDsIsGdg0nAbGjz
++USez/9TGC58ktr/84Kh0gHDE28YSVhsnNSrBJcWaBlYZz4Iy89O2Xc4jbK34Cwg
+Si0ES+Ru1lxLD6FSLYLe43wCIjWRJRrMFcua6nI0P4MCpcKmTkXG2/xz80QSobI3
+z/isqOT54FKHW8DZZVlQMOxh+loeLksfEq7EYVkQoUWEV6xyR24TEpMGfxERgDre
+l7lmx8nIFzRMXkot+P19XWfUBgqctVEiDF4DlRE+SvCZsNCrg7nQuC2AZQIDAQAB
+o0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+1iQqrWM/bCXMk+5c1bulfI5zlKcwDQYJKoZIhvcNAQELBQADggEBAAQO6lw6ePuX
+E+NyhDYCulueMWHC7GRUKa1KpouFT2yM0BSQnP04VakTlwVO3w4w2KucSVVomHR3
+hTY9Ypx7iGLaqdXHmUZvx3uaTM5IXQKMMWL1LJsxAvuzucehgDlOnFBD91tKsr5o
+VRvRU5ya0igBCnnGpFu7NuH3C9pgF01lrQ3EhUHuNeazxleaE3/uQWmAXfxFB4ci
+gHMKSEk3HuYA1raDJFv4ihwO5pXHvlDhcW0C1oMG9lOCh8TXpVzzBDZiH1kWPWSs
+gW9YBu7/p/22U4++X23RyaheGuysfRAMv9cTv+8T0J8NHaAmQz4/QHFXh+0/tQgU
+EVQVGDF6KNU=
+-----END CERTIFICATE-----
+`
+ cfg := signingattr.New()
+ cfg.AddRootCertificateData([]byte(certdata))
+
+ Expect(cfgctx.ApplyConfig(cfg, "from test")).To(Succeed())
+ Expect(rootcertsattr.Get(ocmctx).HasRootCertificates()).To(BeTrue())
+ })
+})
diff --git a/api/ocm/extensions/attrs/signingattr/config.go b/api/ocm/extensions/attrs/signingattr/config.go
new file mode 100644
index 000000000..128e42181
--- /dev/null
+++ b/api/ocm/extensions/attrs/signingattr/config.go
@@ -0,0 +1,296 @@
+package signingattr
+
+import (
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+ "golang.org/x/exp/slices"
+
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/datacontext/attrs/rootcertsattr"
+ "ocm.software/ocm/api/tech/signing"
+ "ocm.software/ocm/api/tech/signing/signutils"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "keys" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+type Issuer struct {
+ CommonName string `json:"commonName,omitempty"`
+ Organization []string `json:"organization,omitempty"`
+ OrganizationalUnit []string `json:"organizationalUnit,omitempty"`
+
+ Country []string `json:"country,omitempty"`
+ Locality []string `json:"locality,omitempty"`
+ Province []string `json:"province,omitempty"`
+ StreetAddress []string `json:"streetAddress,omitempty"`
+ PostalCode []string `json:"postalCode,omitempty"`
+}
+
+func (i *Issuer) Get() *pkix.Name {
+ return &pkix.Name{
+ CommonName: i.CommonName,
+
+ Country: slices.Clone(i.Country),
+ Organization: slices.Clone(i.Organization),
+ OrganizationalUnit: slices.Clone(i.OrganizationalUnit),
+ Locality: slices.Clone(i.Locality),
+ Province: slices.Clone(i.Province),
+ StreetAddress: slices.Clone(i.StreetAddress),
+ PostalCode: slices.Clone(i.PostalCode),
+ }
+}
+
+func (i *Issuer) Set(issuer *pkix.Name) {
+ i.CommonName = issuer.CommonName
+
+ i.Country = slices.Clone(issuer.Country)
+ i.Organization = slices.Clone(issuer.Organization)
+ i.OrganizationalUnit = slices.Clone(issuer.OrganizationalUnit)
+ i.Locality = slices.Clone(issuer.Locality)
+ i.Province = slices.Clone(issuer.Province)
+ i.StreetAddress = slices.Clone(issuer.StreetAddress)
+ i.PostalCode = slices.Clone(issuer.PostalCode)
+}
+
+type KeySpec = cfgcpi.ContentSpec
+
+// Config describes a memory based repository interface.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ PublicKeys map[string]KeySpec `json:"publicKeys,omitempty"`
+ PrivateKeys map[string]KeySpec `json:"privateKeys,omitempty"`
+ Issuers map[string]Issuer `json:"issuers,omitempty"`
+ RootCertificates []KeySpec `json:"rootCertificates,omitempty"`
+ TSAUrl string `json:"tsaURL,omitempty"`
+}
+
+type RawData []byte
+
+var _ json.Unmarshaler = (*RawData)(nil)
+
+func (r RawData) MarshalJSON() ([]byte, error) {
+ return json.Marshal(base64.StdEncoding.EncodeToString(r))
+}
+
+func (r *RawData) UnmarshalJSON(data []byte) error {
+ var s string
+ err := json.Unmarshal(data, &s)
+ if err != nil {
+ return err
+ }
+ *r, err = base64.StdEncoding.DecodeString(s)
+ return err
+}
+
+// New creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(ConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) AddIssuer(name string, issuer *pkix.Name) {
+ var i Issuer
+
+ i.Set(issuer)
+ if a.Issuers == nil {
+ a.Issuers = map[string]Issuer{}
+ }
+ a.Issuers[name] = i
+}
+
+func (a *Config) addKey(set *map[string]KeySpec, name string, key interface{}, conv func(interface{}) *pem.Block) error {
+ if *set == nil {
+ *set = map[string]KeySpec{}
+ }
+ switch data := key.(type) {
+ case []byte:
+ (*set)[name] = KeySpec{Data: data}
+ case string:
+ (*set)[name] = KeySpec{StringData: data}
+ default:
+ if conv != nil {
+ block := conv(key)
+ if block == nil {
+ return errors.ErrUnknown("format")
+ }
+ (*set)[name] = KeySpec{Parsed: key, StringData: string(pem.EncodeToMemory(block))}
+ } else {
+ (*set)[name] = KeySpec{Parsed: key}
+ }
+ }
+ return nil
+}
+
+func (a *Config) AddPublicKey(name string, key interface{}) error {
+ return a.addKey(&a.PublicKeys, name, key, func(key interface{}) *pem.Block { return signutils.PemBlockForPublicKey(key) })
+}
+
+func (a *Config) AddPrivateKey(name string, key interface{}) error {
+ return a.addKey(&a.PrivateKeys, name, key, signutils.PemBlockForPrivateKey)
+}
+
+func (a *Config) addKeyFile(set *map[string]KeySpec, name, path string, fss ...vfs.FileSystem) {
+ var fs vfs.FileSystem
+ for _, fs = range fss {
+ if fs != nil {
+ break
+ }
+ }
+ if *set == nil {
+ *set = map[string]KeySpec{}
+ }
+ (*set)[name] = KeySpec{Path: path, FileSystem: fs}
+}
+
+func (a *Config) AddPublicKeyFile(name, path string, fss ...vfs.FileSystem) {
+ a.addKeyFile(&a.PublicKeys, name, path, fss...)
+}
+
+func (a *Config) AddPrivateKeyFile(name, path string, fss ...vfs.FileSystem) {
+ a.addKeyFile(&a.PrivateKeys, name, path, fss...)
+}
+
+func (a *Config) AddRootCertificateFile(name string, fss ...vfs.FileSystem) {
+ a.RootCertificates = append(a.RootCertificates, KeySpec{Path: name, FileSystem: utils.Optional(fss...)})
+}
+
+func (a *Config) addKeyData(set *map[string]KeySpec, name string, data []byte) {
+ if *set == nil {
+ *set = map[string]KeySpec{}
+ }
+ (*set)[name] = KeySpec{Data: data}
+}
+
+func (a *Config) AddPublicKeyData(name string, data []byte) {
+ a.addKeyData(&a.PublicKeys, name, data)
+}
+
+func (a *Config) AddPrivateKeyData(name string, data []byte) {
+ a.addKeyData(&a.PrivateKeys, name, data)
+}
+
+func (a *Config) AddRootCertificateData(data []byte) {
+ a.RootCertificates = append(a.RootCertificates, KeySpec{Data: data})
+}
+
+func (a *Config) AddRootCertificate(chain signutils.GenericCertificateChain) error {
+ certs, err := signutils.GetCertificateChain(chain, false)
+ if err != nil {
+ return err
+ }
+ a.RootCertificates = append(a.RootCertificates, KeySpec{Data: signutils.CertificateChainToPem(certs), Parsed: certs})
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ t, ok := target.(Context)
+ if !ok {
+ if t, ok := target.(datacontext.AttributesContext); ok {
+ if t.AttributesContext().IsAttributesContext() {
+ return errors.Wrapf(a.ApplyToRootCertsAttr(rootcertsattr.Get(t)), "applying config to certattr failed")
+ }
+ }
+ return cfgcpi.ErrNoContext(ConfigType)
+ }
+ return errors.Wrapf(a.ApplyToRegistry(Get(t)), "applying config failed")
+}
+
+func (a *Config) ApplyToRootCertsAttr(attr *rootcertsattr.Attribute) error {
+ for i, k := range a.RootCertificates {
+ key, err := k.Get()
+ if err != nil {
+ return errors.Wrapf(err, "cannot get root certificate %d", i)
+ }
+ err = attr.RegisterRootCertificates(key)
+ if err != nil {
+ return errors.Wrapf(err, "invalid certificate %d", i)
+ }
+ }
+ return nil
+}
+
+func (a *Config) ApplyToRegistry(registry signing.Registry) error {
+ for n, k := range a.PublicKeys {
+ key, err := k.Get()
+ if err != nil {
+ return errors.Wrapf(err, "cannot get public key %s", n)
+ }
+ registry.RegisterPublicKey(n, key)
+ }
+ for n, k := range a.PrivateKeys {
+ key, err := k.Get()
+ if err != nil {
+ return errors.Wrapf(err, "cannot get private key %s", n)
+ }
+ registry.RegisterPrivateKey(n, key)
+ }
+ for n, k := range a.Issuers {
+ registry.RegisterIssuer(n, k.Get())
+ }
+ if a.TSAUrl != "" {
+ registry.SetTSAUrl(a.TSAUrl)
+ }
+ return nil
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define
+public and private keys. A key value might be given by one of the fields:
+- path
: path of file with key data
+- data
: base64 encoded binary data
+- stringdata
: data a string parsed by key handler
+
++ type: ` + ConfigType + ` + privateKeys: + <name>: + path: <file path> + ... + publicKeys: + <name>: + data: <base64 encoded key representation> + ... + rootCertificates: + - path: <file path> + + issuers: + <name>: + commonName: acme.org ++ +Issuers define an expected distinguished name for +public key certificates optionally provided together with +signatures. They support the following fields: +-
commonName
*string*
+- organization
*string array*
+- organizationalUnit
*string array*
+- country
*string array*
+- locality
*string array*
+- province
*string array*
+- streetAddress
*string array*
+- postalCode
*string array*
+
+At least the given values must be present in the certificate
+to be accepted for a successful signature validation.
+
+`
diff --git a/api/ocm/extensions/attrs/signingattr/setup.go b/api/ocm/extensions/attrs/signingattr/setup.go
new file mode 100644
index 000000000..451141c0d
--- /dev/null
+++ b/api/ocm/extensions/attrs/signingattr/setup.go
@@ -0,0 +1,27 @@
+package signingattr
+
+import (
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/tech/signing"
+)
+
+func init() {
+ datacontext.RegisterSetupHandler(datacontext.SetupHandlerFunction(setupContext))
+}
+
+func setupContext(mode datacontext.BuilderMode, ctx datacontext.Context) {
+ if octx, ok := ctx.(Context); ok {
+ switch mode {
+ case datacontext.MODE_SHARED:
+ fallthrough
+ case datacontext.MODE_DEFAULTED:
+ // do nothing, fallback to the default attribute lookup
+ case datacontext.MODE_EXTENDED:
+ Set(octx, signing.NewRegistry(signing.DefaultRegistry().HandlerRegistry(), signing.DefaultRegistry().KeyRegistry()))
+ case datacontext.MODE_CONFIGURED:
+ Set(octx, signing.DefaultRegistry().Copy())
+ case datacontext.MODE_INITIAL:
+ Set(octx, signing.NewRegistry(nil, nil))
+ }
+ }
+}
diff --git a/pkg/contexts/ocm/attrs/signingattr/suite_test.go b/api/ocm/extensions/attrs/signingattr/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/attrs/signingattr/suite_test.go
rename to api/ocm/extensions/attrs/signingattr/suite_test.go
diff --git a/api/ocm/extensions/blobhandler/config/type.go b/api/ocm/extensions/blobhandler/config/type.go
new file mode 100644
index 000000000..076d8f974
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/config/type.go
@@ -0,0 +1,92 @@
+package config
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/config"
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "uploader.ocm" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+// Config describes a memory based config interface.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Registrations []Registration `json:"registrations,omitempty"`
+}
+
+type Registration struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ blobhandler.HandlerOptions `json:",inline"`
+ Config blobhandler.HandlerConfig `json:"config,omitempty"`
+}
+
+// New creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedObjectType(ConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) AddRegistration(hdlrs ...Registration) error {
+ for i, h := range hdlrs {
+ if h.Name == "" {
+ return fmt.Errorf("handler registration %d requires a name", i)
+ }
+ }
+ a.Registrations = append(a.Registrations, hdlrs...)
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ t, ok := target.(cpi.Context)
+ if !ok {
+ return config.ErrNoContext(ConfigType)
+ }
+ reg := blobhandler.For(t)
+ for _, h := range a.Registrations {
+ accepted, err := reg.RegisterByName(h.Name, t, h.Config, &h.HandlerOptions)
+ if err != nil {
+ return errors.Wrapf(err, "registering upload handler %q[%s]", h.Name, h.Description)
+ }
+ if !accepted {
+ download.Logger(t).Info("no matching handler for configuration %q[%s]", h.Name, h.Description)
+ }
+ }
+ return nil
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define a list
+of pre-configured download handler registrations (see + type: ` + ConfigType + ` + descrition: "my standard download handler configuration" + handlers: + - name: oci/artifact + artifactType: ociImage + mimeType: + config: ... + ... ++` diff --git a/pkg/contexts/ocm/blobhandler/doc.go b/api/ocm/extensions/blobhandler/doc.go similarity index 100% rename from pkg/contexts/ocm/blobhandler/doc.go rename to api/ocm/extensions/blobhandler/doc.go diff --git a/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler.go new file mode 100644 index 000000000..b0cd19c80 --- /dev/null +++ b/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler.go @@ -0,0 +1,131 @@ +package maven + +import ( + "crypto" + + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/goutils/ioutils" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/credentials/builtin/maven/identity" + "ocm.software/ocm/api/ocm/cpi" + access "ocm.software/ocm/api/ocm/extensions/accessmethods/maven" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/tech/maven" + mavenblob "ocm.software/ocm/api/utils/blobaccess/maven" + "ocm.software/ocm/api/utils/iotools" + "ocm.software/ocm/api/utils/logging" + "ocm.software/ocm/api/utils/mime" + "ocm.software/ocm/api/utils/tarutils" +) + +const BlobHandlerName = "ocm/" + resourcetypes.MAVEN_PACKAGE + +type artifactHandler struct { + spec *Config +} + +func NewArtifactHandler(repospec *Config) cpi.BlobHandler { + return &artifactHandler{repospec} +} + +var log = logging.DynamicLogger(identity.REALM) + +func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, resourceType string, hint string, _ cpi.AccessSpec, ctx cpi.StorageContext) (_ cpi.AccessSpec, rerr error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagation(&rerr) + + if hint == "" { + log.Warn("maven package hint is empty, skipping upload") + return nil, nil + } + // check conditions + if b.spec == nil { + return nil, nil + } + mimeType := blob.MimeType() + if resourcetypes.MAVEN_PACKAGE != resourceType { + log.Debug("not a MVN artifact", "resourceType", resourceType) + return nil, nil + } + if mime.MIME_TGZ != mimeType { + log.Debug("not a tarball, can't be a complete maven GAV", "mimeType", mimeType) + return nil, nil + } + + repo, err := b.spec.GetRepository(ctx.GetContext()) + if err != nil { + return nil, err + } + + // setup logger + log := log.WithValues("repository", repo.String()) + // identify artifact + coords, err := maven.Parse(hint) + if err != nil { + return nil, err + } + if !coords.IsPackage() { + return nil, nil + } + log = log.WithValues("groupId", coords.GroupId, "artifactId", coords.ArtifactId, "version", coords.Version) + log.Debug("identified") + + blobReader, err := blob.Reader() + if err != nil { + return nil, err + } + finalize.Close(blobReader) + tempFs, err := tarutils.ExtractTgzToTempFs(blobReader) + if err != nil { + return nil, err + } + finalize.With(func() error { return vfs.Cleanup(tempFs) }) + files, err := tarutils.ListSortedFilesInDir(tempFs, "", false) + if err != nil { + return nil, err + } + for _, file := range files { + loop := finalize.Nested() + log.Debug("uploading", "file", file) + err := coords.SetClassifierExtensionBy(file) + if err != nil { + return nil, err + } + readHash, err := tempFs.Open(file) + if err != nil { + return nil, err + } + loop.Close(readHash) + // MD5 + SHA1 are still the most used ones in the maven context + hr := iotools.NewHashReader(readHash, crypto.SHA256, crypto.SHA1, crypto.MD5) + _, err = hr.CalcHashes() + if err != nil { + return nil, err + } + reader, err := ioutils.NewDupReadCloser(tempFs.Open(file)) + if err != nil { + return nil, err + } + loop.Close(reader) + creds, err := mavenblob.GetCredentials(ctx.GetContext(), repo, coords.GroupId) + if err != nil { + return nil, err + } + err = repo.Upload(coords, reader, creds, hr.Hashes()) + if err != nil { + return nil, err + } + err = loop.Finalize() + if err != nil { + return nil, err + } + } + + log.Debug("done", "artifact", coords) + url, err := repo.Url() + if err != nil { + return nil, err + } + return access.New(url, coords.GroupId, coords.ArtifactId, coords.Version), nil +} diff --git a/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler_test.go b/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler_test.go new file mode 100644 index 000000000..c6147027c --- /dev/null +++ b/api/ocm/extensions/blobhandler/handlers/generic/maven/blobhandler_test.go @@ -0,0 +1,104 @@ +package maven_test + +import ( + "encoding/json" + "os" + + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "ocm.software/ocm/api/helper/builder" + + "github.com/mandelsoft/goutils/sliceutils" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/ocm/elements" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + me "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/maven" + "ocm.software/ocm/api/ocm/extensions/repositories/composition" + "ocm.software/ocm/api/tech/maven" + "ocm.software/ocm/api/tech/maven/maventest" + mavenblob "ocm.software/ocm/api/utils/blobaccess/maven" +) + +const MAVEN_PATH = "/testdata/.m2/repository" + +var _ = Describe("blobhandler generic maven tests", func() { + var env *Builder + var repo *maven.Repository + + BeforeEach(func() { + env = NewBuilder(maventest.TestData()) + repo = maven.NewFileRepository(MAVEN_PATH, env.FileSystem()) + }) + + AfterEach(func() { + env.Cleanup() + }) + + It("Unmarshal upload response Body", func() { + resp := `{ "repo" : "ocm-mvn-test", + "path" : "/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar", + "created" : "2024-04-11T15:09:28.920Z", + "createdBy" : "john.doe", + "downloadUri" : "https://ocm.sofware/repository/ocm-mvn-test/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar", + "mimeType" : "application/java-archive", + "size" : "1792", + "checksums" : { + "sha1" : "99d9acac1ff93ac3d52229edec910091af1bc40a", + "md5" : "6cb7520b65d820b3b35773a8daa8368e", + "sha256" : "b19dcd275f72a0cbdead1e5abacb0ef25a0cb55ff36252ef44b1178eeedf9c30" }, + "originalChecksums" : { + "sha256" : "b19dcd275f72a0cbdead1e5abacb0ef25a0cb55ff36252ef44b1178eeedf9c30" }, + "uri" : "https://ocm.sofware/repository/ocm-mvn-test/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar" }` + var body maven.Body + err := json.Unmarshal([]byte(resp), &body) + Expect(err).To(BeNil()) + Expect(body.Repo).To(Equal("ocm-mvn-test")) + Expect(body.DownloadUri).To(Equal("https://ocm.sofware/repository/ocm-mvn-test/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar")) + Expect(body.Uri).To(Equal("https://ocm.sofware/repository/ocm-mvn-test/open-component-model/hello-ocm/0.0.2/hello-ocm-0.0.2.jar")) + Expect(body.MimeType).To(Equal("application/java-archive")) + Expect(body.Size).To(Equal("1792")) + Expect(body.Checksums["md5"]).To(Equal("6cb7520b65d820b3b35773a8daa8368e")) + Expect(body.Checksums["sha1"]).To(Equal("99d9acac1ff93ac3d52229edec910091af1bc40a")) + Expect(body.Checksums["sha256"]).To(Equal("b19dcd275f72a0cbdead1e5abacb0ef25a0cb55ff36252ef44b1178eeedf9c30")) + Expect(body.Checksums["sha512"]).To(Equal("")) + }) + + It("Upload artifact to file system", func() { + env.OCMContext().BlobHandlers().Register(me.NewArtifactHandler(me.NewFileConfig("target", env.FileSystem()))) + coords := maven.NewCoordinates("com.sap.cloud.sdk", "sdk-modules-bom", "5.7.0") + bacc := Must(mavenblob.BlobAccessForCoords(repo, coords, mavenblob.WithCachingFileSystem(env.FileSystem()))) + defer Close(bacc) + ocmrepo := composition.NewRepository(env) + defer Close(ocmrepo) + cv := composition.NewComponentVersion(env, "acme.org/test", "1.0.0") + MustBeSuccessful(cv.SetResourceBlob(Must(elements.ResourceMeta("test", resourcetypes.MAVEN_PACKAGE)), bacc, coords.GAV(), nil)) + MustBeSuccessful(ocmrepo.AddComponentVersion(cv)) + l := sliceutils.Transform(Must(vfs.ReadDir(env.FileSystem(), "target/com/sap/cloud/sdk/sdk-modules-bom/5.7.0")), + func(info os.FileInfo) string { + return info.Name() + }) + Expect(l).To(ConsistOf( + "sdk-modules-bom-5.7.0-random-content.json", + "sdk-modules-bom-5.7.0-random-content.json.md5", + "sdk-modules-bom-5.7.0-random-content.json.sha1", + "sdk-modules-bom-5.7.0-random-content.json.sha256", + "sdk-modules-bom-5.7.0-random-content.txt", + "sdk-modules-bom-5.7.0-random-content.txt.md5", + "sdk-modules-bom-5.7.0-random-content.txt.sha1", + "sdk-modules-bom-5.7.0-random-content.txt.sha256", + "sdk-modules-bom-5.7.0-sources.jar", + "sdk-modules-bom-5.7.0-sources.jar.md5", + "sdk-modules-bom-5.7.0-sources.jar.sha1", + "sdk-modules-bom-5.7.0-sources.jar.sha256", + "sdk-modules-bom-5.7.0.jar", + "sdk-modules-bom-5.7.0.jar.md5", + "sdk-modules-bom-5.7.0.jar.sha1", + "sdk-modules-bom-5.7.0.jar.sha256", + "sdk-modules-bom-5.7.0.pom", + "sdk-modules-bom-5.7.0.pom.md5", + "sdk-modules-bom-5.7.0.pom.sha1", + "sdk-modules-bom-5.7.0.pom.sha256")) + }) +}) diff --git a/api/ocm/extensions/blobhandler/handlers/generic/maven/registration.go b/api/ocm/extensions/blobhandler/handlers/generic/maven/registration.go new file mode 100644 index 000000000..34b017f3f --- /dev/null +++ b/api/ocm/extensions/blobhandler/handlers/generic/maven/registration.go @@ -0,0 +1,109 @@ +package maven + +import ( + "encoding/json" + "fmt" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/datacontext/attrs/vfsattr" + "ocm.software/ocm/api/ocm/cpi" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/tech/maven" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/mime" + "ocm.software/ocm/api/utils/registrations" +) + +func init() { + cpi.RegisterBlobHandlerRegistrationHandler(BlobHandlerName, &RegistrationHandler{}) +} + +type Config struct { + Url string `json:"url"` + Path string `json:"path"` + FileSystem vfs.FileSystem `json:"-"` +} + +func NewFileConfig(path string, fss ...vfs.FileSystem) *Config { + return &Config{ + Path: path, + FileSystem: utils.FileSystem(fss...), + } +} + +func NewUrlConfig(url string, fss ...vfs.FileSystem) *Config { + return &Config{ + Url: url, + FileSystem: utils.FileSystem(fss...), + } +} + +type rawConfig Config + +func (c *Config) GetRepository(ctx cpi.ContextProvider) (*maven.Repository, error) { + if c.Url != "" && c.Path != "" { + return nil, fmt.Errorf("cannot specify both url and path") + } + if c.Url != "" { + return maven.NewUrlRepository(c.Url, general.OptionalDefaulted(vfsattr.Get(ctx.OCMContext()), c.FileSystem)) + } + if c.Path != "" { + return maven.NewFileRepository(c.Path, general.OptionalDefaulted(vfsattr.Get(ctx.OCMContext()), c.FileSystem)), nil + } + return nil, fmt.Errorf("must specify either url or path") +} + +func (c *Config) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &c.Url) + if err == nil { + return nil + } + var raw rawConfig + err = json.Unmarshal(data, &raw) + if err != nil { + return err + } + *c = Config(raw) + + return nil +} + +type RegistrationHandler struct{} + +var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil) + +func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) { + if handler != "" { + return true, fmt.Errorf("invalid %s handler %q", resourcetypes.MAVEN_PACKAGE, handler) + } + if config == nil { + return true, fmt.Errorf("maven target specification required") + } + cfg, err := registrations.DecodeConfig[Config](config) + if err != nil { + return true, errors.Wrapf(err, "blob handler configuration") + } + + ctx.BlobHandlers().Register(NewArtifactHandler(cfg), + cpi.ForArtifactType(resourcetypes.MAVEN_PACKAGE), + cpi.ForMimeType(mime.MIME_TGZ), + cpi.NewBlobHandlerOptions(olist...), + ) + + return true, nil +} + +func (r *RegistrationHandler) GetHandlers(_ cpi.Context) registrations.HandlerInfos { + return registrations.NewLeafHandlerInfo("uploading maven artifacts", ` +The
`+BlobHandlerName+`
uploader is able to upload maven artifacts (whole GAV only!)
+as artifact archive according to the maven artifact spec.
+If registered the default mime type is: `+mime.MIME_TGZ+`
+
+It accepts a plain string for the URL or a config with the following field:
+'url': the URL of the maven repository.
+`,
+ )
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/maven/registration_test.go b/api/ocm/extensions/blobhandler/handlers/generic/maven/registration_test.go
new file mode 100644
index 000000000..5ba69eb09
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/maven/registration_test.go
@@ -0,0 +1,22 @@
+package maven_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/maven"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+var _ = Describe("Config deserialization Test Environment", func() {
+ It("deserializes string", func() {
+ cfg := Must(registrations.DecodeConfig[maven.Config]("test"))
+ Expect(cfg).To(Equal(&maven.Config{Url: "test"}))
+ })
+
+ It("deserializes struct", func() {
+ cfg := Must(registrations.DecodeConfig[maven.Config](`{"url":"test"}`))
+ Expect(cfg).To(Equal(&maven.Config{Url: "test"}))
+ })
+})
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/maven/suite_test.go b/api/ocm/extensions/blobhandler/handlers/generic/maven/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/maven/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/generic/maven/suite_test.go
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/npm/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/blobhandler.go
new file mode 100644
index 000000000..d37cebe7d
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/npm/blobhandler.go
@@ -0,0 +1,172 @@
+package npm
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+
+ crds "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/npm"
+ npmLogin "ocm.software/ocm/api/tech/npm"
+ "ocm.software/ocm/api/utils/logging"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const BLOB_HANDLER_NAME = "ocm/npmPackage"
+
+type artifactHandler struct {
+ spec *Config
+}
+
+func NewArtifactHandler(repospec *Config) cpi.BlobHandler {
+ return &artifactHandler{repospec}
+}
+
+func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
+ if b.spec == nil {
+ return nil, nil
+ }
+
+ mimeType := blob.MimeType()
+ if mime.MIME_TGZ != mimeType && mime.MIME_TGZ_ALT != mimeType {
+ return nil, nil
+ }
+
+ if b.spec.Url == "" {
+ return nil, fmt.Errorf("NPM registry url not provided")
+ }
+
+ blobReader, err := blob.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer blobReader.Close()
+
+ data, err := io.ReadAll(blobReader)
+ if err != nil {
+ return nil, err
+ }
+
+ // read package.json from tarball to get name, version, etc.
+ log := logging.Context().Logger(npmLogin.REALM)
+ log.Debug("reading package.json from tarball")
+ var pkg *Package
+ pkg, err = prepare(data)
+ if err != nil {
+ return nil, err
+ }
+ tbName := pkg.Name + "-" + pkg.Version + ".tgz"
+ pkg.Dist.Tarball = b.spec.Url + pkg.Name + "/-/" + tbName
+ log = log.WithValues("package", pkg.Name, "version", pkg.Version)
+ log.Debug("identified")
+
+ // check if package exists
+ exists, err := packageExists(b.spec.Url, *pkg, ctx.GetContext())
+ if err != nil {
+ return nil, err
+ }
+ if exists {
+ log.Debug("package+version already exists, skipping upload")
+ return npm.New(b.spec.Url, pkg.Name, pkg.Version), nil
+ }
+
+ // prepare body for upload
+ body := Body{
+ ID: pkg.Name,
+ Name: pkg.Name,
+ Description: pkg.Description,
+ }
+ body.Versions = map[string]*Package{
+ pkg.Version: pkg,
+ }
+ body.DistTags.Latest = pkg.Version
+ body.Readme = pkg.Readme
+ body.Attachments = map[string]*Attachment{
+ tbName: NewAttachment(data),
+ }
+ marshal, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+
+ // prepare PUT request
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, b.spec.Url+"/"+url.PathEscape(pkg.Name), bytes.NewReader(marshal))
+ if err != nil {
+ return nil, err
+ }
+ err = npmLogin.Authorize(req, ctx.GetContext(), b.spec.Url, pkg.Name)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ // send PUT request - upload tgz
+ client := http.Client{}
+ log.Debug("uploading")
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusCreated {
+ all, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ return nil, fmt.Errorf("http (%d) - failed to upload package: %s", resp.StatusCode, string(all))
+ }
+ log.Debug("successfully uploaded")
+ return npm.New(b.spec.Url, pkg.Name, pkg.Version), nil
+}
+
+// Check if package already exists in npm registry. If it does, checks if it's the same.
+func packageExists(repoUrl string, pkg Package, ctx crds.ContextProvider) (bool, error) {
+ client := http.Client{}
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, repoUrl+"/"+url.PathEscape(pkg.Name)+"/"+url.PathEscape(pkg.Version), nil)
+ if err != nil {
+ return false, err
+ }
+ err = npmLogin.Authorize(req, ctx, repoUrl, pkg.Name)
+ if err != nil {
+ return false, err
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNotFound {
+ // artifact doesn't exist, it's safe to upload
+ return false, nil
+ }
+
+ // artifact exists, let's check if it's the same
+ all, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return false, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return false, fmt.Errorf("http (%d) - %s", resp.StatusCode, string(all))
+ }
+ var data map[string]interface{}
+ err = json.Unmarshal(all, &data)
+ if err != nil {
+ return false, err
+ }
+ dist := data["dist"].(map[string]interface{})
+ if pkg.Dist.Integrity == dist["integrity"] {
+ // sha-512 sum is the same, we can skip the upload
+ return true, nil
+ }
+ if pkg.Dist.Shasum == dist["shasum"] {
+ // sha-1 sum is the same, we can skip the upload
+ return true, nil
+ }
+
+ return false, fmt.Errorf("artifact already exists but has different shasum or integrity")
+}
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/publish.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go
rename to api/ocm/extensions/blobhandler/handlers/generic/npm/publish.go
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish_test.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/publish_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish_test.go
rename to api/ocm/extensions/blobhandler/handlers/generic/npm/publish_test.go
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/npm/registration.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/registration.go
new file mode 100644
index 000000000..fc95d89d7
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/npm/registration.go
@@ -0,0 +1,75 @@
+package npm
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/utils/mime"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+type Config struct {
+ Url string `json:"url"`
+}
+
+type rawConfig Config
+
+func (c *Config) UnmarshalJSON(data []byte) error {
+ err := json.Unmarshal(data, &c.Url)
+ if err == nil {
+ return nil
+ }
+ var raw rawConfig
+ err = json.Unmarshal(data, &raw)
+ if err != nil {
+ return err
+ }
+ *c = Config(raw)
+
+ return nil
+}
+
+func init() {
+ cpi.RegisterBlobHandlerRegistrationHandler(BLOB_HANDLER_NAME, &RegistrationHandler{})
+}
+
+type RegistrationHandler struct{}
+
+var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) {
+ if handler != "" {
+ return true, fmt.Errorf("invalid npmjsArtifact handler %q", handler)
+ }
+ if config == nil {
+ return true, fmt.Errorf("npm target specification required")
+ }
+ cfg, err := registrations.DecodeConfig[Config](config)
+ if err != nil {
+ return true, errors.Wrapf(err, "blob handler configuration")
+ }
+
+ ctx.BlobHandlers().Register(NewArtifactHandler(cfg),
+ cpi.ForArtifactType(resourcetypes.NPM_PACKAGE),
+ cpi.ForMimeType(mime.MIME_TGZ),
+ cpi.NewBlobHandlerOptions(olist...),
+ )
+
+ return true, nil
+}
+
+func (r *RegistrationHandler) GetHandlers(_ cpi.Context) registrations.HandlerInfos {
+ return registrations.NewLeafHandlerInfo("uploading npm artifacts", `
+The `+BLOB_HANDLER_NAME+`
uploader is able to upload npm artifacts
+as artifact archive according to the npm package spec.
+If registered the default mime type is: `+mime.MIME_TGZ+`
+
+It accepts a plain string for the URL or a config with the following field:
+'url': the URL of the npm repository.
+`,
+ )
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/npm/registration_test.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/registration_test.go
new file mode 100644
index 000000000..f18e85098
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/npm/registration_test.go
@@ -0,0 +1,22 @@
+package npm_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/npm"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+var _ = Describe("Config deserialization Test Environment", func() {
+ It("deserializes string", func() {
+ cfg := Must(registrations.DecodeConfig[npm.Config]("test"))
+ Expect(cfg).To(Equal(&npm.Config{Url: "test"}))
+ })
+
+ It("deserializes struct", func() {
+ cfg := Must(registrations.DecodeConfig[npm.Config](`{"url":"test"}`))
+ Expect(cfg).To(Equal(&npm.Config{Url: "test"}))
+ })
+})
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/suite_test.go b/api/ocm/extensions/blobhandler/handlers/generic/npm/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/generic/npm/suite_test.go
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/testdata/testdata.tgz b/api/ocm/extensions/blobhandler/handlers/generic/npm/testdata/testdata.tgz
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/npm/testdata/testdata.tgz
rename to api/ocm/extensions/blobhandler/handlers/generic/npm/testdata/testdata.tgz
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/blobhandler.go
new file mode 100644
index 000000000..2c48ea05b
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/blobhandler.go
@@ -0,0 +1,126 @@
+package ocirepo
+
+import (
+ "encoding/json"
+ "path"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/sliceutils"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+func init() {
+ for _, mime := range artdesc.ArchiveBlobTypes() {
+ cpi.RegisterBlobHandler(NewArtifactHandler(), cpi.ForMimeType(mime), cpi.WithPrio(10))
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// artifactHandler stores artifact blobs as OCIArtifacts regardless of the
+// intended OCM target repository.
+// It acts on the OCI upload attribute to determine the target OCI repository.
+// If none is configured, it does nothing.
+type artifactHandler struct {
+ spec *ociuploadattr.Attribute
+}
+
+func NewArtifactHandler(repospec ...*ociuploadattr.Attribute) cpi.BlobHandler {
+ return &artifactHandler{utils.Optional(repospec...)}
+}
+
+func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
+ attr := b.spec
+ if attr == nil {
+ attr = ociuploadattr.Get(ctx.GetContext())
+ }
+ if attr == nil {
+ return nil, nil
+ }
+
+ mediaType := blob.MimeType()
+ if !artdesc.IsOCIMediaType(mediaType) || (!strings.HasSuffix(mediaType, "+tar") && !strings.HasSuffix(mediaType, "+tar+gzip")) {
+ return nil, nil
+ }
+
+ repo, base, prefix, err := attr.GetInfo(ctx.GetContext())
+ if err != nil {
+ return nil, err
+ }
+
+ // this section is for logging, only
+ target, err := json.Marshal(repo.GetSpecification())
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot marshal target specification")
+ }
+ values := []interface{}{
+ "arttype", artType,
+ "mediatype", mediaType,
+ "hint", hint,
+ "target", string(target),
+ }
+ if m, ok := blob.(blobaccess.AnnotatedBlobAccess[cpi.AccessMethod]); ok {
+ // prepare for optimized point to point implementation
+ cpi.BlobHandlerLogger(ctx.GetContext()).Debug("oci generic artifact handler with ocm access source",
+ sliceutils.CopyAppend[any](values, "sourcetype", m.Source().AccessSpec().GetType())...,
+ )
+ } else {
+ cpi.BlobHandlerLogger(ctx.GetContext()).Debug("oci generic artifact handler", values...)
+ }
+
+ var namespace oci.NamespaceAccess
+ var version string
+ var name string
+ var tag string
+
+ if hint == "" {
+ name = path.Join(prefix, ctx.TargetComponentName())
+ } else {
+ i := strings.LastIndex(hint, ":")
+ if i > 0 {
+ version = hint[i:]
+ name = path.Join(prefix, hint[:i])
+ tag = version[1:] // remove colon
+ } else {
+ name = hint
+ }
+ }
+ namespace, err = repo.LookupNamespace(name)
+ if err != nil {
+ return nil, errors.Wrapf(err, "lookup namespace %s in target repository %s", name, attr.Ref)
+ }
+ defer namespace.Close()
+
+ set, err := artifactset.OpenFromBlob(accessobj.ACC_READONLY, blob)
+ if err != nil {
+ return nil, err
+ }
+ defer set.Close()
+ digest := set.GetMain()
+ if version == "" {
+ version = "@" + digest.String()
+ }
+ art, err := set.GetArtifact(digest.String())
+ if err != nil {
+ return nil, err
+ }
+ defer art.Close()
+
+ err = artifactset.TransferArtifact(art, namespace, oci.AsTags(tag)...)
+ if err != nil {
+ return nil, err
+ }
+
+ ref := base.ComposeRef(namespace.GetNamespace() + version)
+ return ociartifact.New(ref), nil
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/registration.go b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/registration.go
new file mode 100644
index 000000000..8bb6a2cb5
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/registration.go
@@ -0,0 +1,77 @@
+package ocirepo
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/utils/listformat"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+type Config = ociuploadattr.Attribute
+
+func init() {
+ cpi.RegisterBlobHandlerRegistrationHandler("ocm/ociArtifacts", &RegistrationHandler{})
+}
+
+type RegistrationHandler struct{}
+
+var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) {
+ if handler != "" {
+ return true, fmt.Errorf("invalid ociArtifact handler %q", handler)
+ }
+ if config == nil {
+ return true, fmt.Errorf("oci target specification required")
+ }
+ attr, err := registrations.DecodeConfig[Config](config, ociuploadattr.AttributeType{}.Decode)
+ if err != nil {
+ return true, errors.Wrapf(err, "blob handler configuration")
+ }
+
+ var mimes []string
+ opts := cpi.NewBlobHandlerOptions(olist...)
+ if opts.MimeType != "" {
+ found := false
+ for _, a := range artdesc.ArchiveBlobTypes() {
+ if a == opts.MimeType {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return true, fmt.Errorf("unexpected type mime type %q for oci blob handler target", opts.MimeType)
+ }
+ mimes = append(mimes, opts.MimeType)
+ } else {
+ mimes = artdesc.ArchiveBlobTypes()
+ }
+
+ h := NewArtifactHandler(attr)
+ for _, m := range mimes {
+ opts.MimeType = m
+ ctx.BlobHandlers().Register(h, opts)
+ }
+
+ return true, nil
+}
+
+func (r *RegistrationHandler) GetHandlers(ctx cpi.Context) registrations.HandlerInfos {
+ return registrations.NewLeafHandlerInfo("downloading OCI artifacts", `
+The ociArtifacts
downloader is able to download OCI artifacts
+as artifact archive according to the OCI distribution spec.
+The following artifact media types are supported:
+`+listformat.FormatList("", artdesc.ArchiveBlobTypes()...)+`
+By default, it is registered for these mimetypes.
+
+It accepts a config with the following fields:
+`+listformat.FormatMapElements("", ociuploadattr.AttributeDescription())+`
+Alternatively, a single string value can be given representing an OCI repository
+reference.`,
+ )
+}
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/ocirepo/suite_test.go b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/ocirepo/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/generic/ocirepo/suite_test.go
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/upload_test.go b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/upload_test.go
new file mode 100644
index 000000000..758973d3f
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/ocirepo/upload_test.go
@@ -0,0 +1,244 @@
+package ocirepo_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/oci"
+ ctfoci "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ "ocm.software/ocm/api/oci/grammar"
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler"
+ "ocm.software/ocm/api/ocm/extensions/repositories/comparch"
+ ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/tools/transfer"
+ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+)
+
+const (
+ COMP = "github.com/compa"
+ VERS = "1.0.0"
+ CA = "ca"
+ CTF = "ctf"
+ COPY = "ctf.copy"
+ TARGET = "/tmp/target"
+)
+
+const (
+ OCIHOST = "alias"
+ OCIPATH = "/tmp/source"
+)
+
+var _ = Describe("upload", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+
+ // fake OCI registry
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ OCIManifest1(env)
+ })
+
+ env.OCICommonTransport(TARGET, accessio.FormatDirectory)
+
+ env.ComponentArchive(CA, accessio.FormatDirectory, COMP, VERS, func() {
+ env.Provider("mandelsoft")
+ env.Resource("value", "", resourcetypes.OCI_IMAGE, v1.LocalRelation, func() {
+ env.Access(
+ ociartifact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION)),
+ )
+ })
+ })
+
+ ca := Must(comparch.Open(env.OCMContext(), accessobj.ACC_READONLY, CA, 0, env))
+ oca := accessio.OnceCloser(ca)
+ defer Close(oca)
+
+ ctf := Must(ctfocm.Create(env.OCMContext(), accessobj.ACC_CREATE, CTF, 0o700, env))
+ octf := accessio.OnceCloser(ctf)
+ defer Close(octf)
+
+ handler := Must(standard.New(standard.ResourcesByValue()))
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, ca, ctf, handler))
+
+ // now we have a transport archive with local blob for the image
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("validated original oci manifest", func() {
+ ctx := env.OCMContext()
+
+ ocirepo := Must(ctfoci.Open(ctx, accessobj.ACC_READONLY, OCIPATH, 0o700, env))
+ defer Close(ocirepo, "ocoirepo")
+
+ ns := Must(ocirepo.LookupNamespace(OCINAMESPACE))
+ defer Close(ns, "namespace")
+
+ art := Must(ns.GetArtifact(OCIVERSION))
+ defer Close(art, "artifact")
+
+ Expect(art.Digest().Encoded()).To(Equal(D_OCIMANIFEST1))
+ })
+
+ It("validated original digest", func() {
+ ctx := env.OCMContext()
+
+ ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0o700, env))
+ defer Close(ctf, "ctf")
+
+ cv := Must(ctf.LookupComponentVersion(COMP, VERS))
+ defer Close(cv, "component version")
+
+ ra := Must(cv.GetResourceByIndex(0))
+ acc := Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(localblob.Type))
+
+ Expect(ra.Meta().Digest).To(Equal(DS_OCIMANIFEST1))
+ })
+
+ It("transfers oci artifact", func() {
+ ctx := env.OCMContext()
+
+ ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0o700, env))
+ defer Close(ctf, "ctf")
+
+ cv := Must(ctf.LookupComponentVersion(COMP, VERS))
+ ocv := accessio.OnceCloser(cv)
+ defer Close(ocv)
+ ra := Must(cv.GetResourceByIndex(0))
+ acc := Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(localblob.Type))
+
+ // transfer component
+ copy := Must(ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0o700, env))
+ ocopy := accessio.OnceCloser(copy)
+ defer Close(ocopy)
+
+ // prepare upload to target OCI repo
+ attr := ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy")
+ ociuploadattr.Set(ctx, attr)
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, copy, nil))
+
+ // check type
+ cv2 := Must(copy.LookupComponentVersion(COMP, VERS))
+ ocv2 := accessio.OnceCloser(cv2)
+ defer Close(ocv2)
+ ra = Must(cv2.GetResourceByIndex(0))
+ Expect(ra.Meta().Digest).To(Equal(DS_OCIMANIFEST1))
+ acc = Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(ociartifact.Type))
+ val := Must(ctx.AccessSpecForSpec(acc))
+ // TODO: the result is invalid for ctf: better handling for ctf refs
+ Expect(val.(*ociartifact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0"))
+
+ attr.Close()
+ target, err := ctfoci.Open(ctx.OCIContext(), accessobj.ACC_READONLY, TARGET, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ Expect(target.ExistsArtifact("copy/ocm/value", "v2.0")).To(BeTrue())
+ })
+
+ It("transfers oci artifact with named handler and object config", func() {
+ ctx := env.OCMContext()
+
+ ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0o700, env))
+ defer Close(ctf, "ctf")
+
+ cv := Must(ctf.LookupComponentVersion(COMP, VERS))
+ ocv := accessio.OnceCloser(cv)
+ defer Close(ocv)
+ ra := Must(cv.GetResourceByIndex(0))
+ acc := Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(localblob.Type))
+
+ // transfer component
+ copy := Must(ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0o700, env))
+ ocopy := accessio.OnceCloser(copy)
+ defer Close(ocopy)
+
+ // prepare upload to target OCI repo
+ attr := ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy")
+ MustBeSuccessful(blobhandler.RegisterHandlerByName(ctx, "ocm/ociArtifacts", attr))
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, copy, nil))
+
+ // check type
+ cv2 := Must(copy.LookupComponentVersion(COMP, VERS))
+ ocv2 := accessio.OnceCloser(cv2)
+ defer Close(ocv2)
+ ra = Must(cv2.GetResourceByIndex(0))
+ acc = Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(ociartifact.Type))
+ val := Must(ctx.AccessSpecForSpec(acc))
+ // TODO: the result is invalid for ctf: better handling for ctf refs
+ Expect(val.(*ociartifact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0"))
+
+ // attr.Close()
+ env.OCMContext().Finalize()
+ target, err := ctfoci.Open(ctx.OCIContext(), accessobj.ACC_READONLY, TARGET, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ Expect(target.ExistsArtifact("copy/ocm/value", "v2.0")).To(BeTrue())
+ })
+
+ It("transfers oci artifact with named handler and string config", func() {
+ ctx := env.OCMContext()
+
+ ctf := Must(ctfocm.Open(ctx, accessobj.ACC_READONLY, CTF, 0o700, env))
+ defer Close(ctf, "ctf")
+
+ cv := Must(ctf.LookupComponentVersion(COMP, VERS))
+ ocv := accessio.OnceCloser(cv)
+ defer Close(ocv)
+ ra := Must(cv.GetResourceByIndex(0))
+ acc := Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(localblob.Type))
+
+ // transfer component
+ copy := Must(ctfocm.Create(ctx, accessobj.ACC_CREATE, COPY, 0o700, env))
+ ocopy := accessio.OnceCloser(copy)
+ defer Close(ocopy)
+
+ // prepare upload to target OCI repo
+ // attr := ociuploadattr.New(TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy")
+ attr := TARGET + grammar.RepositorySeparator + grammar.RepositorySeparator + "copy"
+ MustBeSuccessful(blobhandler.RegisterHandlerByName(ctx, "ocm/ociArtifacts", attr))
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, copy, nil))
+
+ // check type
+ cv2 := Must(copy.LookupComponentVersion(COMP, VERS))
+ ocv2 := accessio.OnceCloser(cv2)
+ defer Close(ocv2)
+ ra = Must(cv2.GetResourceByIndex(0))
+ acc = Must(ra.Access())
+ Expect(acc.GetKind()).To(Equal(ociartifact.Type))
+ val := Must(ctx.AccessSpecForSpec(acc))
+ // TODO: the result is invalid for ctf: better handling for ctf refs
+ Expect(val.(*ociartifact.AccessSpec).ImageReference).To(Equal("/tmp/target//copy/ocm/value:v2.0"))
+
+ // attr.Close()
+ env.OCMContext().Finalize()
+ target, err := ctfoci.Open(ctx.OCIContext(), accessobj.ACC_READONLY, TARGET, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ Expect(target.ExistsArtifact("copy/ocm/value", "v2.0")).To(BeTrue())
+ })
+})
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/plugin/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/generic/plugin/blobhandler.go
new file mode 100644
index 000000000..c9ae369c4
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/plugin/blobhandler.go
@@ -0,0 +1,89 @@
+package plugin
+
+import (
+ "encoding/json"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/utils/accessio"
+)
+
+// pluginHandler delegates storage of blobs to a plugin based handler.
+type pluginHandler struct {
+ plugin plugin.Plugin
+ name string
+ target json.RawMessage
+ targetinfo *plugin.UploadTargetSpecInfo
+}
+
+func New(p plugin.Plugin, name string, target json.RawMessage) (cpi.BlobHandler, error) {
+ var err error
+
+ ud := p.GetUploaderDescriptor(name)
+ if ud == nil {
+ return nil, errors.ErrUnknown(descriptor.KIND_UPLOADER, name, p.Name())
+ }
+
+ var info *plugin.UploadTargetSpecInfo
+ if target != nil {
+ info, err = p.ValidateUploadTarget(name, target)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &pluginHandler{
+ plugin: p,
+ name: name,
+ target: target,
+ targetinfo: info,
+ }, nil
+}
+
+func (b *pluginHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (acc cpi.AccessSpec, err error) {
+ var creds credentials.Credentials
+
+ if b.targetinfo != nil {
+ if len(b.targetinfo.ConsumerId) > 0 {
+ creds, err = credentials.CredentialsForConsumer(ctx.GetContext(), b.targetinfo.ConsumerId, hostpath.IdentityMatcher(b.targetinfo.ConsumerId.Type()))
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ target := b.target
+
+ if b.target == nil {
+ target, err = json.Marshal(ctx.TargetComponentRepository().GetSpecification())
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot marshal target repo spec")
+ }
+ }
+
+ cpi.BlobHandlerLogger(ctx.GetContext()).Debug("plugin blob handler",
+ "plugin", b.plugin.Name(),
+ "uploader", b.name,
+ "arttype", artType,
+ "mediatype", blob.MimeType(),
+ "hint", hint,
+ "target", string(target),
+ )
+
+ var creddata json.RawMessage
+ if creds != nil {
+ creddata, err = json.Marshal(creds.Properties())
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot marshal credentials")
+ }
+ }
+
+ r := accessio.NewOndemandReader(blob)
+ defer errors.PropagateError(&err, r.Close)
+
+ return b.plugin.Put(b.name, r, artType, blob.MimeType(), hint, creddata, target)
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/plugin/registration.go b/api/ocm/extensions/blobhandler/handlers/generic/plugin/registration.go
new file mode 100644
index 000000000..5a4743e43
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/plugin/registration.go
@@ -0,0 +1,120 @@
+package plugin
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+type Config = json.RawMessage
+
+func init() {
+ cpi.RegisterBlobHandlerRegistrationHandler("plugin", &RegistrationHandler{})
+}
+
+type RegistrationHandler struct{}
+
+var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) {
+ path := cpi.NewNamePath(handler)
+
+ if config == nil {
+ return true, fmt.Errorf("target specification required")
+ }
+
+ if len(path) < 1 || len(path) > 2 {
+ return true, fmt.Errorf("plugin handler must be of the form <plugin name>/<handler>
")
+
+ set := plugincacheattr.Get(ctx)
+ if set == nil {
+ return infos
+ }
+
+ for _, name := range set.PluginNames() {
+ p := set.Get(name)
+ if !p.IsValid() {
+ continue
+ }
+ for _, u := range p.GetDescriptor().Uploaders {
+ i := registrations.HandlerInfo{
+ Name: name + "/" + u.GetName(),
+ ShortDesc: "",
+ Description: u.GetDescription(),
+ }
+ infos = append(infos, i)
+ }
+ }
+ return infos
+}
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/suite_test.go b/api/ocm/extensions/blobhandler/handlers/generic/plugin/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/plugin/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/generic/plugin/suite_test.go
diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/plugin/testdata/test b/api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/generic/plugin/testdata/test
rename to api/ocm/extensions/blobhandler/handlers/generic/plugin/testdata/test
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/plugin/upload_test.go b/api/ocm/extensions/blobhandler/handlers/generic/plugin/upload_test.go
new file mode 100644
index 000000000..01d794590
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/generic/plugin/upload_test.go
@@ -0,0 +1,201 @@
+//go:build unix
+
+package plugin_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
+
+ "github.com/mandelsoft/filepath/pkg/filepath"
+ "github.com/mandelsoft/vfs/pkg/osfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/plugin"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ plugin2 "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/config"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+ "ocm.software/ocm/api/ocm/tools/transfer"
+ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const PLUGIN = "test"
+
+const (
+ ARCH = "ctf"
+ OUT = "/tmp/res"
+ COMP = "github.com/mandelsoft/comp"
+ VERS = "1.0.0"
+ PROVIDER = "mandelsoft"
+ RSCTYPE = "TestArtifact"
+ MEDIA = "text/plain"
+)
+
+const (
+ REPOTYPE = "test/v1"
+ ACCTYPE = "test/v1"
+ REPO = "plugin"
+ CONTENT = "some test content\n"
+ HINT = "given"
+)
+
+type RepoSpec struct {
+ runtime.ObjectVersionedType
+ Path string `json:"path"`
+}
+
+func NewRepoSpec(path string) *RepoSpec {
+ return &RepoSpec{
+ ObjectVersionedType: runtime.ObjectVersionedType{Type: REPOTYPE},
+ Path: path,
+ }
+}
+
+type AccessSpec struct {
+ runtime.ObjectVersionedType
+ Path string `json:"path"`
+ MediaType string `json:"mediaType"`
+ Repository string `json:"repo"`
+}
+
+func NewAccessSpec(media, path, repo string) *AccessSpec {
+ return &AccessSpec{
+ ObjectVersionedType: runtime.ObjectVersionedType{Type: ACCTYPE},
+ MediaType: media,
+ Path: path,
+ Repository: repo,
+ }
+}
+
+var _ = Describe("setup plugin cache", func() {
+ var ctx ocm.Context
+ var registry plugins.Set
+ var repodir string
+ var env *Builder
+ var plugins TempPluginDir
+
+ accessSpec := NewAccessSpec(MEDIA, "given", REPO)
+ repoSpec := NewRepoSpec(REPO)
+
+ BeforeEach(func() {
+ repodir = Must(os.MkdirTemp(os.TempDir(), "uploadtest-*"))
+
+ env = NewBuilder(nil)
+ ctx = env.OCMContext()
+ plugins, registry = Must2(ConfigureTestPlugins2(env, "testdata"))
+ p := registry.Get("test")
+ Expect(p).NotTo(BeNil())
+
+ ctx.ConfigContext().ApplyConfig(config.New(PLUGIN, []byte(fmt.Sprintf(`{"root": "`+repodir+`"}`))), "plugin config")
+ registration.RegisterExtensions(ctx)
+
+ env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() {
+ env.Component(COMP, func() {
+ env.Version(VERS, func() {
+ env.Provider(PROVIDER)
+ env.Resource("testdata", VERS, RSCTYPE, metav1.LocalRelation, func() {
+ env.Hint(HINT)
+ env.BlobStringData(MEDIA, CONTENT)
+ // env.Access(NewAccessSpec(MEDIA, "given", "dummy"))
+ })
+ })
+ })
+ })
+ })
+
+ AfterEach(func() {
+ plugins.Cleanup()
+ env.Cleanup()
+ os.RemoveAll(repodir)
+ })
+
+ It("uploads artifact", func() {
+ repo := Must(ctf.Open(ctx, accessobj.ACC_READONLY, ARCH, 0, env))
+ defer Close(repo, "source repo")
+
+ cv := Must(repo.LookupComponentVersion(COMP, VERS))
+ defer Close(cv, "source version")
+
+ _, _, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", []byte("{}"))
+ fmt.Printf("error %q\n", err)
+ MustFailWithMessage(err, "plugin uploader test/testuploader: error processing plugin command upload: path missing in repository spec")
+ repospec := Must(json.Marshal(repoSpec))
+ name, keys, err := plugin.RegisterBlobHandler(env.OCMContext(), "test", "", RSCTYPE, "", repospec)
+ MustBeSuccessful(err)
+ Expect(name).To(Equal("testuploader"))
+ Expect(keys).To(Equal(plugin2.UploaderKeySet{}.Add(plugin2.UploaderKey{}.SetArtifact(RSCTYPE, ""))))
+
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer Close(tgt, "target repo")
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, Must(standard.New(standard.ResourcesByValue()))))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ Expect(vfs.FileExists(osfs.New(), filepath.Join(repodir, REPO, HINT))).To(BeTrue())
+
+ tcv := Must(tgt.LookupComponentVersion(COMP, VERS))
+ defer Close(tcv, "target version")
+
+ r := Must(tcv.GetResourceByIndex(0))
+ a := Must(r.Access())
+
+ var spec AccessSpec
+ MustBeSuccessful(json.Unmarshal(Must(json.Marshal(a)), &spec))
+ Expect(spec).To(Equal(*accessSpec))
+
+ m := Must(a.AccessMethod(tcv))
+ defer Close(m, "method")
+
+ Expect(string(Must(m.Get()))).To(Equal(CONTENT))
+ })
+
+ It("uploads after abstract registration", func() {
+ repo := Must(ctf.Open(ctx, accessobj.ACC_READONLY, ARCH, 0, env))
+ defer Close(repo, "source repo")
+
+ cv := Must(repo.LookupComponentVersion(COMP, VERS))
+ defer Close(cv, "source version")
+
+ MustFailWithMessage(blobhandler.RegisterHandlerByName(ctx, "plugin/test", []byte("{}"), blobhandler.ForArtifactType(RSCTYPE)),
+ "plugin uploader test/testuploader: error processing plugin command upload: path missing in repository spec")
+ repospec := Must(json.Marshal(repoSpec))
+ MustBeSuccessful(blobhandler.RegisterHandlerByName(ctx, "plugin/test", repospec))
+
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer Close(tgt, "target repo")
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, Must(standard.New(standard.ResourcesByValue()))))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ Expect(vfs.FileExists(osfs.New(), filepath.Join(repodir, REPO, HINT))).To(BeTrue())
+
+ tcv := Must(tgt.LookupComponentVersion(COMP, VERS))
+ defer Close(tcv, "target version")
+
+ r := Must(tcv.GetResourceByIndex(0))
+ a := Must(r.Access())
+
+ var spec AccessSpec
+ MustBeSuccessful(json.Unmarshal(Must(json.Marshal(a)), &spec))
+ Expect(spec).To(Equal(*accessSpec))
+
+ m := Must(a.AccessMethod(tcv))
+ defer Close(m, "method")
+
+ Expect(string(Must(m.Get()))).To(Equal(CONTENT))
+ })
+})
diff --git a/api/ocm/extensions/blobhandler/handlers/init.go b/api/ocm/extensions/blobhandler/handlers/init.go
new file mode 100644
index 000000000..f2b911839
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/init.go
@@ -0,0 +1,9 @@
+package handlers
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/maven"
+ _ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/npm"
+ _ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/ocirepo"
+ _ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci/ocirepo"
+ _ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/ocm/comparch"
+)
diff --git a/api/ocm/extensions/blobhandler/handlers/oci/ctx.go b/api/ocm/extensions/blobhandler/handlers/oci/ctx.go
new file mode 100644
index 000000000..f7e4012a7
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/oci/ctx.go
@@ -0,0 +1,76 @@
+package oci
+
+import (
+ "reflect"
+
+ ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/cpi"
+ ocmcpi "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg/componentmapping"
+)
+
+// StorageContext is the context information passed for Blobhandler
+// registered for context type oci.CONTEXT_TYPE.
+type StorageContext struct {
+ ocmcpi.DefaultStorageContext
+ Repository cpi.Repository
+ Namespace cpi.NamespaceAccess
+ Manifest cpi.ManifestAccess
+}
+
+var _ ocmcpi.StorageContext = (*StorageContext)(nil)
+
+func New(compname string, repo ocmcpi.Repository, impltyp string, ocirepo oci.Repository, namespace oci.NamespaceAccess, manifest oci.ManifestAccess) *StorageContext {
+ return &StorageContext{
+ DefaultStorageContext: *ocmcpi.NewDefaultStorageContext(
+ repo,
+ compname,
+ ocmcpi.ImplementationRepositoryType{
+ ContextType: cpi.CONTEXT_TYPE,
+ RepositoryType: impltyp,
+ },
+ ),
+ Repository: ocirepo,
+ Namespace: namespace,
+ Manifest: manifest,
+ }
+}
+
+func (s *StorageContext) TargetComponentRepository() ocmcpi.Repository {
+ return s.ComponentRepository
+}
+
+func (s *StorageContext) TargetComponentName() string {
+ return s.ComponentName
+}
+
+func (s *StorageContext) AssureLayer(blob cpi.BlobAccess) error {
+ return AssureLayer(s.Manifest.GetDescriptor(), blob)
+}
+
+func AssureLayer(desc *artdesc.Manifest, blob cpi.BlobAccess) error {
+ d := artdesc.DefaultBlobDescriptor(blob)
+
+ found := -1
+ for i, l := range desc.Layers {
+ if reflect.DeepEqual(&desc.Layers[i], d) {
+ return nil
+ }
+ if l.Digest == blob.Digest() {
+ found = i
+ }
+ }
+ if found > 0 { // ignore layer 0 used for component descriptor
+ desc.Layers[found] = *d
+ } else {
+ if len(desc.Layers) == 0 {
+ // fake descriptor layer
+ desc.Layers = append(desc.Layers, ociv1.Descriptor{MediaType: componentmapping.ComponentDescriptorConfigMimeType})
+ }
+ desc.Layers = append(desc.Layers, *d)
+ }
+ return nil
+}
diff --git a/pkg/contexts/ocm/blobhandler/handlers/oci/doc.go b/api/ocm/extensions/blobhandler/handlers/oci/doc.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/oci/doc.go
rename to api/ocm/extensions/blobhandler/handlers/oci/doc.go
diff --git a/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/blobhandler.go
new file mode 100644
index 000000000..91ea3d21a
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/blobhandler.go
@@ -0,0 +1,347 @@
+package ocirepo
+
+import (
+ "fmt"
+ "path"
+ "strings"
+
+ . "github.com/mandelsoft/goutils/finalizer"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/sliceutils"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/extensions/repositories/ocireg"
+ "ocm.software/ocm/api/oci/grammar"
+ "ocm.software/ocm/api/oci/tools/transfer"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localociblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociblob"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compatattr"
+ "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ "ocm.software/ocm/api/ocm/extensions/attrs/mapocirepoattr"
+ storagecontext "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+func init() {
+ for _, mime := range artdesc.ArchiveBlobTypes() {
+ cpi.RegisterBlobHandler(NewArtifactHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.Type),
+ cpi.ForMimeType(mime))
+ cpi.RegisterBlobHandler(NewArtifactHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.LegacyType),
+ cpi.ForMimeType(mime))
+ cpi.RegisterBlobHandler(NewArtifactHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.ShortType),
+ cpi.ForMimeType(mime))
+ }
+ /*
+ cpi.RegisterBlobHandler(NewBlobHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.Type))
+ cpi.RegisterBlobHandler(NewBlobHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.LegacyType))
+ cpi.RegisterBlobHandler(NewBlobHandler(OCIRegBaseFunction), cpi.ForRepo(oci.CONTEXT_TYPE, ocireg.ShortType))
+ */
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type BaseFunction func(ctx *storagecontext.StorageContext) string
+
+func OCIRegBaseFunction(ctx *storagecontext.StorageContext) string {
+ i, err := ocireg.GetRepositoryImplementation(ctx.Repository)
+ if err != nil {
+ panic("ocireg implementation mismatch")
+ }
+ return i.GetBaseURL()
+}
+
+// blobHandler is the default handling to store local blobs as local blobs but with an additional
+// globally accessible OCIBlob access method.
+type blobHandler struct {
+ base BaseFunction
+}
+
+func (h *blobHandler) GetBaseURL(ctx *storagecontext.StorageContext) string {
+ if h.base == nil {
+ return ""
+ }
+ return h.base(ctx)
+}
+
+func NewBlobHandler(base BaseFunction) cpi.BlobHandler {
+ return &blobHandler{base}
+}
+
+func (b *blobHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
+ ocictx, ok := ctx.(*storagecontext.StorageContext)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to storagecontext.StorageContext", ctx)
+ }
+
+ values := []interface{}{
+ "arttype", artType,
+ "mediatype", blob.MimeType(),
+ "hint", hint,
+ }
+ if m, ok := blob.(blobaccess.AnnotatedBlobAccess[accspeccpi.AccessMethodView]); ok {
+ cpi.BlobHandlerLogger(ctx.GetContext()).Debug("oci blob handler with ocm access source",
+ sliceutils.CopyAppend[any](values, "sourcetype", m.Source().AccessSpec().GetType())...,
+ )
+ } else {
+ cpi.BlobHandlerLogger(ctx.GetContext()).Debug("oci blob handler", values...)
+ }
+
+ err := ocictx.Manifest.AddBlob(blob)
+ if err != nil {
+ return nil, err
+ }
+ err = ocictx.AssureLayer(blob)
+ if err != nil {
+ return nil, err
+ }
+ if compatattr.Get(ctx.GetContext()) {
+ return localociblob.New(blob.Digest()), nil
+ } else {
+ if global == nil {
+ base := b.GetBaseURL(ocictx)
+ if base != "" {
+ global = ociblob.New(path.Join(base, ocictx.Namespace.GetNamespace()), blob.Digest(), blob.MimeType(), blob.Size())
+ }
+ }
+ return localblob.New(blob.Digest().String(), "", blob.MimeType(), global), nil
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// artifactHandler stores artifact blobs as OCIArtifacts.
+type artifactHandler struct {
+ blobHandler
+}
+
+func NewArtifactHandler(base BaseFunction) cpi.BlobHandler {
+ return &artifactHandler{blobHandler{base}}
+}
+
+func (b *artifactHandler) CheckBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (bool, bool, error) {
+ mediaType := blob.MimeType()
+
+ if !artdesc.IsOCIMediaType(mediaType) || (!strings.HasSuffix(mediaType, "+tar") && !strings.HasSuffix(mediaType, "+tar+gzip")) {
+ return false, false, nil
+ }
+
+ log := cpi.BlobHandlerLogger(ctx.GetContext())
+
+ values := []interface{}{
+ "arttype", artType,
+ "mediatype", mediaType,
+ "hint", hint,
+ }
+
+ var art oci.ArtifactAccess
+ var err error
+ var finalizer Finalizer
+ defer finalizer.Finalize()
+
+ var namespace oci.NamespaceAccess
+ var version string
+ var name string
+ var tag string
+
+ ocictx, ok := ctx.(*storagecontext.StorageContext)
+ if !ok {
+ return false, false, fmt.Errorf("failed to assert type %T to storagecontext.StorageContext", ctx)
+ }
+ if hint == "" {
+ namespace = ocictx.Namespace
+ } else {
+ prefix := cpi.RepositoryPrefix(ctx.TargetComponentRepository().GetSpecification())
+ i := strings.LastIndex(hint, "@")
+ if i >= 0 {
+ hint = hint[:i] // remove digest
+ }
+ i = strings.LastIndex(hint, ":")
+ if i > 0 {
+ version = hint[i:]
+ tag = version[1:] // remove colon
+ name = hint[:i]
+ } else {
+ name = hint
+ }
+
+ hash := mapocirepoattr.Get(ctx.GetContext())
+ if hash.Prefix != nil {
+ prefix = *hash.Prefix
+ }
+ orig := name
+ mapped := hash.Map(name)
+ name = path.Join(prefix, mapped)
+ if mapped == orig {
+ log.Debug("namespace derived from hint",
+ sliceutils.CopyAppend[any](values, "namespace", name),
+ )
+ } else {
+ log.Debug("mapped namespace derived from hint",
+ sliceutils.CopyAppend[any](values, "namespace", name),
+ )
+ }
+
+ namespace, err = ocictx.Repository.LookupNamespace(name)
+ if err != nil {
+ return false, false, err
+ }
+ defer namespace.Close()
+ }
+
+ ok, err = namespace.HasArtifact(string(art.Digest()))
+ if ok {
+ return true, true, err
+ }
+ ok, err = namespace.HasArtifact(tag)
+ return ok, true, err
+}
+
+func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
+ mediaType := blob.MimeType()
+
+ if !artdesc.IsOCIMediaType(mediaType) || (!strings.HasSuffix(mediaType, "+tar") && !strings.HasSuffix(mediaType, "+tar+gzip")) {
+ return nil, nil
+ }
+
+ errhint := "[" + hint + "]"
+ log := cpi.BlobHandlerLogger(ctx.GetContext())
+
+ values := []interface{}{
+ "arttype", artType,
+ "mediatype", mediaType,
+ "hint", hint,
+ }
+
+ var art oci.ArtifactAccess
+ var err error
+ var finalizer Finalizer
+ defer finalizer.Finalize()
+
+ keep := keepblobattr.Get(ctx.GetContext())
+
+ if m, ok := blob.(blobaccess.AnnotatedBlobAccess[accspeccpi.AccessMethodView]); ok {
+ // prepare for optimized point to point implementation
+ log.Debug("oci artifact handler with ocm access source",
+ sliceutils.CopyAppend[any](values, "sourcetype", m.Source().AccessSpec().GetType())...,
+ )
+ if ocimeth, ok := m.Source().Unwrap().(ociartifact.AccessMethodImpl); !keep && ok {
+ art, _, err = ocimeth.GetArtifact()
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot access source artifact")
+ }
+ if art != nil {
+ defer art.Close()
+ }
+ }
+ } else {
+ log.Debug("oci artifact handler", values...)
+ }
+
+ var namespace oci.NamespaceAccess
+ var version string
+ var name string
+ var tag string
+ var digest digest.Digest
+
+ ocictx, ok := ctx.(*storagecontext.StorageContext)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to storagecontext.StorageContext", ctx)
+ }
+ base := b.GetBaseURL(ocictx)
+ if hint == "" {
+ namespace = ocictx.Namespace
+ } else {
+ prefix := cpi.RepositoryPrefix(ctx.TargetComponentRepository().GetSpecification())
+ i := strings.LastIndex(hint, "@")
+ if i >= 0 {
+ hint = hint[:i] // remove digest
+ }
+ i = strings.LastIndex(hint, ":")
+ if i > 0 {
+ version = hint[i:]
+ tag = version[1:] // remove colon
+ name = hint[:i]
+ } else {
+ name = hint
+ }
+
+ hash := mapocirepoattr.Get(ctx.GetContext())
+ if hash.Prefix != nil {
+ prefix = *hash.Prefix
+ }
+ orig := name
+ mapped := hash.Map(name)
+ name = path.Join(prefix, mapped)
+ if mapped == orig {
+ log.Debug("namespace derived from hint",
+ sliceutils.CopyAppend[any](values, "namespace", name),
+ )
+ } else {
+ log.Debug("mapped namespace derived from hint",
+ sliceutils.CopyAppend[any](values, "namespace", name),
+ )
+ }
+
+ namespace, err = ocictx.Repository.LookupNamespace(name)
+ if err != nil {
+ return nil, err
+ }
+ defer namespace.Close()
+ }
+
+ errhint += " namespace " + namespace.GetNamespace()
+
+ if art == nil {
+ log.Debug("using artifact set transfer mode")
+ set, err := artifactset.OpenFromBlob(accessobj.ACC_READONLY, blob)
+ if err != nil {
+ return nil, wrap(err, errhint, "open blob")
+ }
+ defer set.Close()
+ digest = set.GetMain()
+ art, err = set.GetArtifact(digest.String())
+ if err != nil {
+ return nil, wrap(err, errhint, "get artifact from blob")
+ }
+ defer art.Close()
+ } else {
+ log.Debug("using direct transfer mode")
+ digest = art.Digest()
+ }
+
+ if version == "" {
+ version = "@" + digest.String()
+ }
+
+ err = transfer.TransferArtifact(art, namespace, oci.AsTags(tag)...)
+ if err != nil {
+ return nil, wrap(err, errhint, "transfer artifact")
+ }
+ match := grammar.AnchoredSchemedRegexp.FindStringSubmatch(base)
+ scheme := ""
+ if match != nil {
+ scheme = match[1]
+ base = match[2]
+ }
+ if scheme != "" {
+ scheme += "://"
+ }
+ ref := scheme + path.Join(base, namespace.GetNamespace()) + version
+ return ociartifact.New(ref), nil
+}
+
+func wrap(err error, msg string, args ...interface{}) error {
+ for _, a := range args {
+ msg = fmt.Sprintf("%s: %s", msg, a)
+ }
+ return errors.Wrapf(err, "exploding OCI artifact resource blob (%s)", msg)
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go
new file mode 100644
index 000000000..855a6244a
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go
@@ -0,0 +1,245 @@
+package ocirepo_test
+
+import (
+ "encoding/json"
+ "fmt"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/oci/testhelper"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ ocictf "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/attrs/keepblobattr"
+ "ocm.software/ocm/api/ocm/extensions/attrs/mapocirepoattr"
+ storagecontext "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci"
+ "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/oci/ocirepo"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg"
+ "ocm.software/ocm/api/ocm/tools/transfer"
+ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const (
+ ARCH = "/tmp/ctf"
+ ARCH2 = "/tmp/ctf2"
+ PROVIDER = "mandelsoft"
+ VERSION = "v1"
+ COMPONENT = "github.com/mandelsoft/test"
+ COMPONENT2 = "github.com/mandelsoft/test2"
+ OUT = "/tmp/res"
+ OCIPATH = "/tmp/oci"
+ OCIHOST = "alias"
+)
+
+func FakeOCIRegBaseFunction(ctx *storagecontext.StorageContext) string {
+ return "baseurl.io"
+}
+
+var _ = Describe("oci artifact transfer", func() {
+ var env *Builder
+ var ldesc *artdesc.Descriptor
+
+ BeforeEach(func() {
+ env = NewBuilder()
+
+ FakeOCIRepo(env, OCIPATH, OCIHOST)
+
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ ldesc = OCIManifest1(env)
+ })
+
+ env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() {
+ env.Component(COMPONENT, func() {
+ env.Version(VERSION, func() {
+ env.Provider(PROVIDER)
+ env.Resource("testdata", "", "PlainText", metav1.LocalRelation, func() {
+ env.BlobStringData(mime.MIME_TEXT, "testdata")
+ })
+ env.Resource("artifact", "", resourcetypes.OCI_IMAGE, metav1.LocalRelation, func() {
+ env.Access(
+ ociartifact.New(oci.StandardOCIRef(OCIHOST+".alias", OCINAMESPACE, OCIVERSION)),
+ )
+ })
+ })
+ })
+ })
+
+ _ = ldesc
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("it should copy a resource by value and export the OCI image but keep the local blob", func() {
+ env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction),
+ cpi.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), cpi.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)))
+ keepblobattr.Set(env.OCMContext(), true)
+
+ src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env))
+ cv := Must(src.LookupComponentVersion(COMPONENT, VERSION))
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer tgt.Close()
+
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, handler))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list := Must(tgt.ComponentLister().GetComponents("", true))
+ Expect(list).To(Equal([]string{COMPONENT}))
+ comp := Must(tgt.LookupComponentVersion(COMPONENT, VERSION))
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(2))
+ data := Must(json.Marshal(comp.GetDescriptor().Resources[1].Access))
+
+ fmt.Printf("%s\n", string(data))
+ Expect(string(data)).To(StringEqualWithContext(`{"globalAccess":{"imageReference":"baseurl.io/ocm/value:v2.0","type":"ociArtifact"},"localReference":"sha256:b0692bcec00e0a875b6b280f3209d6776f3eca128adcb7e81e82fd32127c0c62","mediaType":"application/vnd.oci.image.manifest.v1+tar+gzip","referenceName":"ocm/value:v2.0","type":"localBlob"}`))
+ ocirepo := genericocireg.GetOCIRepository(tgt)
+ Expect(ocirepo).NotTo(BeNil())
+
+ art := Must(ocirepo.LookupArtifact(OCINAMESPACE, OCIVERSION))
+ defer Close(art, "artifact")
+
+ man := MustBeNonNil(art.ManifestAccess())
+ Expect(len(man.GetDescriptor().Layers)).To(Equal(1))
+ Expect(man.GetDescriptor().Layers[0].Digest).To(Equal(ldesc.Digest))
+
+ blob := Must(man.GetBlob(ldesc.Digest))
+ data = Must(blob.Get())
+ Expect(string(data)).To(Equal(OCILAYER))
+ })
+
+ It("it should copy a resource by value and export the OCI image", func() {
+ env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction),
+ cpi.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), cpi.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)))
+
+ src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env))
+ cv := Must(src.LookupComponentVersion(COMPONENT, VERSION))
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer tgt.Close()
+
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, handler))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list := Must(tgt.ComponentLister().GetComponents("", true))
+ Expect(list).To(Equal([]string{COMPONENT}))
+ comp := Must(tgt.LookupComponentVersion(COMPONENT, VERSION))
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(2))
+ data := Must(json.Marshal(comp.GetDescriptor().Resources[1].Access))
+
+ fmt.Printf("%s\n", string(data))
+ Expect(string(data)).To(StringEqualWithContext("{\"imageReference\":\"baseurl.io/ocm/value:v2.0\",\"type\":\"ociArtifact\"}"))
+
+ ocirepo := genericocireg.GetOCIRepository(tgt)
+ art := Must(ocirepo.LookupArtifact(OCINAMESPACE, OCIVERSION))
+ defer Close(art, "artifact")
+
+ man := MustBeNonNil(art.ManifestAccess())
+ Expect(len(man.GetDescriptor().Layers)).To(Equal(1))
+ Expect(man.GetDescriptor().Layers[0].Digest).To(Equal(ldesc.Digest))
+
+ blob := Must(man.GetBlob(ldesc.Digest))
+ data = Must(blob.Get())
+ Expect(string(data)).To(Equal(OCILAYER))
+ })
+
+ It("it should copy a resource by value and export the OCI image with hashed repo name", func() {
+ env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction),
+ cpi.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), cpi.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)))
+
+ src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env))
+ cv := Must(src.LookupComponentVersion(COMPONENT, VERSION))
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer tgt.Close()
+
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+ mapocirepoattr.Set(env.OCMContext(), &mapocirepoattr.Attribute{Mode: mapocirepoattr.ShortHashMode, Always: true})
+ rdigest := "e9b6af2174cb2fb78b2882a1f487b01295b8f6bfa7e4c1ceb350440104c9ce65"
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, handler))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list := Must(tgt.ComponentLister().GetComponents("", true))
+ Expect(list).To(Equal([]string{COMPONENT}))
+ comp := Must(tgt.LookupComponentVersion(COMPONENT, VERSION))
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(2))
+ data := Must(json.Marshal(comp.GetDescriptor().Resources[1].Access))
+
+ fmt.Printf("%s\n", string(data))
+ Expect(string(data)).To(StringEqualWithContext("{\"imageReference\":\"baseurl.io/" + rdigest[:8] + "/value:v2.0\",\"type\":\"ociArtifact\"}"))
+
+ namespace := rdigest[:8] + "/value"
+ ocirepo := genericocireg.GetOCIRepository(tgt)
+ art := Must(ocirepo.LookupArtifact(namespace, OCIVERSION))
+ defer Close(art, "artifact")
+
+ man := MustBeNonNil(art.ManifestAccess())
+ Expect(len(man.GetDescriptor().Layers)).To(Equal(1))
+ Expect(man.GetDescriptor().Layers[0].Digest).To(Equal(ldesc.Digest))
+
+ blob := Must(man.GetBlob(ldesc.Digest))
+ data = Must(blob.Get())
+ Expect(string(data)).To(Equal(OCILAYER))
+ })
+
+ It("it should copy a resource by value and export the OCI image with hashed repo name and prefix", func() {
+ env.OCMContext().BlobHandlers().Register(ocirepo.NewArtifactHandler(FakeOCIRegBaseFunction),
+ cpi.ForRepo(oci.CONTEXT_TYPE, ocictf.Type), cpi.ForMimeType(artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest)))
+
+ src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env))
+ cv := Must(src.LookupComponentVersion(COMPONENT, VERSION))
+ tgt := Must(ctf.Create(env.OCMContext(), accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, OUT, 0o700, accessio.FormatDirectory, env))
+ defer tgt.Close()
+
+ opts := &standard.Options{}
+ opts.SetResourcesByValue(true)
+ handler := standard.NewDefaultHandler(opts)
+ prefix := "ocm"
+ mapocirepoattr.Set(env.OCMContext(), &mapocirepoattr.Attribute{Mode: mapocirepoattr.ShortHashMode, Always: true, Prefix: &prefix})
+ rdigest := "e9b6af2174cb2fb78b2882a1f487b01295b8f6bfa7e4c1ceb350440104c9ce65"
+
+ MustBeSuccessful(transfer.TransferVersion(nil, nil, cv, tgt, handler))
+ Expect(env.DirExists(OUT)).To(BeTrue())
+
+ list := Must(tgt.ComponentLister().GetComponents("", true))
+ Expect(list).To(Equal([]string{COMPONENT}))
+ comp := Must(tgt.LookupComponentVersion(COMPONENT, VERSION))
+ Expect(len(comp.GetDescriptor().Resources)).To(Equal(2))
+ data := Must(json.Marshal(comp.GetDescriptor().Resources[1].Access))
+
+ fmt.Printf("%s\n", string(data))
+ Expect(string(data)).To(StringEqualWithContext("{\"imageReference\":\"baseurl.io/ocm/" + rdigest[:8] + "/value:v2.0\",\"type\":\"ociArtifact\"}"))
+
+ namespace := "ocm/" + rdigest[:8] + "/value"
+ ocirepo := genericocireg.GetOCIRepository(tgt)
+ art := Must(ocirepo.LookupArtifact(namespace, OCIVERSION))
+ defer Close(art, "artifact")
+
+ man := MustBeNonNil(art.ManifestAccess())
+ Expect(len(man.GetDescriptor().Layers)).To(Equal(1))
+ Expect(man.GetDescriptor().Layers[0].Digest).To(Equal(ldesc.Digest))
+
+ blob := Must(man.GetBlob(ldesc.Digest))
+ data = Must(blob.Get())
+ Expect(string(data)).To(Equal(OCILAYER))
+ })
+})
diff --git a/pkg/contexts/ocm/blobhandler/handlers/oci/ocirepo/suite_test.go b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/oci/ocirepo/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/oci/ocirepo/suite_test.go
diff --git a/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler.go
new file mode 100644
index 000000000..a057a1666
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler.go
@@ -0,0 +1,50 @@
+package comparch
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localfsblob"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compatattr"
+ storagecontext "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/ocm"
+ "ocm.software/ocm/api/ocm/extensions/repositories/comparch"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+func init() {
+ cpi.RegisterBlobHandler(NewBlobHandler(), cpi.ForRepo(cpi.CONTEXT_TYPE, comparch.Type))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// blobHandler is the default handling to store local blobs as local blobs.
+type blobHandler struct{}
+
+func NewBlobHandler() cpi.BlobHandler {
+ return &blobHandler{}
+}
+
+func (b *blobHandler) StoreBlob(blob cpi.BlobAccess, artType, hint string, global cpi.AccessSpec, ctx cpi.StorageContext) (cpi.AccessSpec, error) {
+ ocmctx, ok := ctx.(storagecontext.StorageContext)
+ if !ok {
+ return nil, fmt.Errorf("failed to assert type %T to storagecontext.StorageContext", ctx)
+ }
+
+ if blob == nil {
+ return nil, errors.New("a resource has to be defined")
+ }
+ ref, err := ocmctx.AddBlob(blob)
+ if err != nil {
+ return nil, err
+ }
+ path := common.DigestToFileName(digest.Digest(ref))
+ if compatattr.Get(ctx.GetContext()) {
+ return localfsblob.New(path, blob.MimeType()), nil
+ } else {
+ return localblob.New(path, hint, blob.MimeType(), global), nil
+ }
+}
diff --git a/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler_test.go b/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler_test.go
new file mode 100644
index 000000000..c021f3509
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/ocm/comparch/blobhandler_test.go
@@ -0,0 +1,81 @@
+package comparch_test
+
+import (
+ "encoding/json"
+ "reflect"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/helper/builder"
+ "ocm.software/ocm/api/helper/env"
+ "ocm.software/ocm/api/ocm/compdesc"
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localfsblob"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compatattr"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const ARCHIVE = "archive"
+
+var _ = Describe("blobhandler", func() {
+ Context("regular", func() {
+ var b *builder.Builder
+
+ BeforeEach(func() {
+ b = builder.NewBuilder()
+ })
+
+ AfterEach(func() {
+ b.Cleanup()
+ })
+
+ It("uses generic local access", func() {
+ b.ComponentArchive(ARCHIVE, accessio.FormatDirectory, "github.com/mandelsoft/test", "1.0.0", func() {
+ b.Resource("test", "1.0.0", "Test", v1.LocalRelation, func() {
+ b.BlobStringData(mime.MIME_TEXT, "testdata")
+ })
+ })
+ data := Must(b.ReadFile(vfs.Join(b, ARCHIVE, compdesc.ComponentDescriptorFileName)))
+ cd := Must(compdesc.Decode(data))
+ Expect(cd.Resources[0].Access.GetType()).To(Equal(localblob.Type))
+
+ data = Must(json.Marshal(cd.Resources[0].Access))
+ found := Must(localblob.Decode(data))
+ spec := localblob.New("sha256.810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50", "", mime.MIME_TEXT, nil)
+ Expect(found).To(Equal(spec))
+ })
+ })
+ Context("legacy", func() {
+ var b *builder.Builder
+ BeforeEach(func() {
+ b = builder.NewBuilder(env.NewEnvironment())
+ Expect(b.ConfigContext().GetAttributes().SetAttribute(compatattr.ATTR_KEY, true)).To(Succeed())
+ })
+ AfterEach(func() {
+ b.Cleanup()
+ })
+ It("uses generic local access", func() {
+ b.ComponentArchive(ARCHIVE, accessio.FormatDirectory, "github.com/mandelsoft/test", "1.0.0", func() {
+ b.Resource("test", "1.0.0", "Test", v1.LocalRelation, func() {
+ b.BlobStringData(mime.MIME_TEXT, "testdata")
+ })
+ })
+ data := Must(b.ReadFile(vfs.Join(b, ARCHIVE, compdesc.ComponentDescriptorFileName)))
+ cd := Must(compdesc.Decode(data))
+ Expect(cd.Resources[0].Access.GetType()).To(Equal(localfsblob.Type))
+
+ data = Must(json.Marshal(cd.Resources[0].Access))
+ found := Must(localfsblob.Decode(data))
+
+ spec := localfsblob.New("sha256.810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50", mime.MIME_TEXT)
+ reflect.DeepEqual(found, spec)
+ Expect(found).To(Equal(spec))
+ })
+ })
+})
diff --git a/pkg/contexts/ocm/blobhandler/handlers/ocm/comparch/suite_test.go b/api/ocm/extensions/blobhandler/handlers/ocm/comparch/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/ocm/comparch/suite_test.go
rename to api/ocm/extensions/blobhandler/handlers/ocm/comparch/suite_test.go
diff --git a/api/ocm/extensions/blobhandler/handlers/ocm/ctx.go b/api/ocm/extensions/blobhandler/handlers/ocm/ctx.go
new file mode 100644
index 000000000..829c262a9
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/handlers/ocm/ctx.go
@@ -0,0 +1,36 @@
+package ocm
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+type BlobSink interface {
+ AddBlob(blob blobaccess.BlobAccess) (string, error)
+}
+
+// StorageContext is the context information passed for Blobhandler
+// registered for context type oci.CONTEXT_TYPE.
+type StorageContext interface {
+ cpi.StorageContext
+ BlobSink
+}
+
+type DefaultStorageContext struct {
+ cpi.DefaultStorageContext
+ Sink BlobSink
+ Payload interface{}
+}
+
+func New(repo cpi.Repository, compname string, access BlobSink, impltyp string, payload ...interface{}) StorageContext {
+ return &DefaultStorageContext{
+ DefaultStorageContext: *cpi.NewDefaultStorageContext(repo, compname, cpi.ImplementationRepositoryType{cpi.CONTEXT_TYPE, impltyp}),
+ Sink: access,
+ Payload: utils.Optional(payload...),
+ }
+}
+
+func (c *DefaultStorageContext) AddBlob(blob blobaccess.BlobAccess) (string, error) {
+ return c.Sink.AddBlob(blob)
+}
diff --git a/pkg/contexts/ocm/blobhandler/handlers/ocm/doc.go b/api/ocm/extensions/blobhandler/handlers/ocm/doc.go
similarity index 100%
rename from pkg/contexts/ocm/blobhandler/handlers/ocm/doc.go
rename to api/ocm/extensions/blobhandler/handlers/ocm/doc.go
diff --git a/api/ocm/extensions/blobhandler/interface.go b/api/ocm/extensions/blobhandler/interface.go
new file mode 100644
index 000000000..c3fa5c645
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/interface.go
@@ -0,0 +1,17 @@
+package blobhandler
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+type (
+ HandlerConfig = cpi.BlobHandlerConfig
+ HandlerOption = cpi.BlobHandlerOption
+ HandlerOptions = cpi.BlobHandlerOptions
+ HandlerRegistry = cpi.BlobHandlerRegistry
+ HandlerKey = cpi.BlobHandlerKey
+)
+
+func For(ctx cpi.ContextProvider) cpi.BlobHandlerRegistry {
+ return ctx.OCMContext().BlobHandlers()
+}
diff --git a/api/ocm/extensions/blobhandler/registration.go b/api/ocm/extensions/blobhandler/registration.go
new file mode 100644
index 000000000..83a176351
--- /dev/null
+++ b/api/ocm/extensions/blobhandler/registration.go
@@ -0,0 +1,34 @@
+package blobhandler
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+func RegisterHandlerByName(ctx cpi.ContextProvider, name string, config HandlerConfig, opts ...HandlerOption) error {
+ o, err := For(ctx).RegisterByName(name, ctx.OCMContext(), config, opts...)
+ if err != nil {
+ return err
+ }
+ if !o {
+ return fmt.Errorf("no matching handler found for %q", name)
+ }
+ return nil
+}
+
+func WithPrio(prio int) HandlerOption {
+ return cpi.WithPrio(prio)
+}
+
+func ForArtifactType(t string) HandlerOption {
+ return cpi.ForArtifactType(t)
+}
+
+func ForMimeType(t string) HandlerOption {
+ return cpi.ForMimeType(t)
+}
+
+func ForRepo(ctxtype string, repotype string) HandlerOption {
+ return cpi.ForRepo(ctxtype, repotype)
+}
diff --git a/api/ocm/extensions/digester/digesters/artifact/digester.go b/api/ocm/extensions/digester/digesters/artifact/digester.go
new file mode 100644
index 000000000..64800aab0
--- /dev/null
+++ b/api/ocm/extensions/digester/digesters/artifact/digester.go
@@ -0,0 +1,174 @@
+package artifact
+
+import (
+ "archive/tar"
+ "fmt"
+ "io"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/tech/signing"
+ "ocm.software/ocm/api/tech/signing/hasher/sha256"
+ "ocm.software/ocm/api/tech/signing/hasher/sha512"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/compression"
+)
+
+const OciArtifactDigestV1 string = "ociArtifactDigest/v1"
+
+const LegacyOciArtifactDigestV1 string = "ociArtefactDigest/v1"
+
+func init() {
+ cpi.MustRegisterDigester(New(sha256.Algorithm), "")
+ cpi.MustRegisterDigester(New(sha512.Algorithm), "")
+
+ // legacy digester types
+ cpi.MustRegisterDigester(New(digest.SHA256.String(), OciArtifactDigestV1), "")
+ cpi.MustRegisterDigester(New(digest.SHA512.String(), OciArtifactDigestV1), "")
+
+ cpi.MustRegisterDigester(New(digest.SHA256.String(), LegacyOciArtifactDigestV1), "")
+ cpi.MustRegisterDigester(New(digest.SHA512.String(), LegacyOciArtifactDigestV1), "")
+}
+
+func New(algo string, ts ...string) cpi.BlobDigester {
+ norm := general.OptionalDefaulted(OciArtifactDigestV1, ts...)
+ return &Digester{
+ cpi.DigesterType{
+ HashAlgorithm: algo,
+ NormalizationAlgorithm: norm,
+ },
+ }
+}
+
+type Digester struct {
+ typ cpi.DigesterType
+}
+
+var _ cpi.BlobDigester = (*Digester)(nil)
+
+func (d *Digester) GetType() cpi.DigesterType {
+ return d.typ
+}
+
+func (d *Digester) DetermineDigest(reftyp string, method cpi.AccessMethod, preferred signing.Hasher) (*cpi.DigestDescriptor, error) {
+ if method.IsLocal() {
+ mime := method.MimeType()
+ if !artdesc.IsOCIMediaType(mime) {
+ return nil, nil
+ }
+ r, err := method.Reader()
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ var reader io.ReadCloser
+ reader, _, err = compression.AutoDecompress(r)
+ if err != nil {
+ return nil, err
+ }
+ defer reader.Close()
+ tr := tar.NewReader(reader)
+
+ var desc *cpi.DigestDescriptor
+ oci := false
+ layout := false
+ for {
+ header, err := tr.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ if oci {
+ if layout {
+ return desc, nil
+ } else {
+ err = fmt.Errorf("oci-layout not found")
+ }
+ } else {
+ err = fmt.Errorf("descriptor not found in archive")
+ }
+ }
+ return nil, errors.ErrInvalidWrap(err, "artifact archive")
+ }
+
+ switch header.Typeflag {
+ case tar.TypeDir:
+ case tar.TypeReg:
+ switch header.Name {
+ case artifactset.OCILayouFileName:
+ layout = true
+ case artifactset.OCIArtifactSetDescriptorFileName:
+ oci = true
+ fallthrough
+ case artifactset.ArtifactSetDescriptorFileName:
+ data, err := io.ReadAll(tr)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read descriptor from archive: %w", err)
+ }
+ index, err := artdesc.DecodeIndex(data)
+ if err != nil {
+ return nil, err
+ }
+ if index == nil {
+ return nil, fmt.Errorf("no main artifact found")
+ }
+ main := artifactset.RetrieveMainArtifactFromIndex(index)
+ if main == "" {
+ return nil, fmt.Errorf("no main artifact found")
+ }
+ dig := artifactset.RetrieveDigest(index, main)
+ if dig == "" {
+ return nil, fmt.Errorf("no main artifact digest found for %s", main)
+ }
+ if d.GetType().HashAlgorithm != signing.NormalizeHashAlgorithm(string(dig.Algorithm())) {
+ return nil, nil
+ }
+ desc = cpi.NewDigestDescriptor(dig.Hex(), d.GetType())
+ if !oci {
+ return desc, nil
+ }
+ }
+ }
+ }
+ // not reached (endless for)
+ }
+ if ociartifact.Is(method.AccessSpec()) {
+ var (
+ dig digest.Digest
+ err error
+ )
+
+ impl := accspeccpi.GetAccessMethodImplementation(method)
+ // first, ask access specification (inexpensive)
+ if s, ok := method.AccessSpec().(blobaccess.DigestSource); ok {
+ dig = s.Digest()
+ }
+ if dig == "" {
+ // second: check for error providing interface
+ if s, ok := impl.(accspeccpi.DigestSource); ok {
+ dig, err = s.GetDigest()
+ }
+ }
+ if dig == "" && err == nil {
+ // third: fallback to standard digest interface
+ if s, ok := impl.(blobaccess.DigestSource); ok {
+ dig = s.Digest()
+ }
+ }
+
+ if dig != "" {
+ if d.GetType().HashAlgorithm != signing.NormalizeHashAlgorithm(dig.Algorithm().String()) {
+ return nil, nil
+ }
+ return cpi.NewDigestDescriptor(dig.Hex(), d.GetType()), nil
+ }
+ return nil, errors.NewEf(err, "cannot determine digest")
+ }
+ return nil, nil
+}
diff --git a/api/ocm/extensions/digester/digesters/blob/digester.go b/api/ocm/extensions/digester/digesters/blob/digester.go
new file mode 100644
index 000000000..3132d1db2
--- /dev/null
+++ b/api/ocm/extensions/digester/digesters/blob/digester.go
@@ -0,0 +1,45 @@
+package blob
+
+import (
+ "fmt"
+ "io"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/tech/signing"
+)
+
+const GenericBlobDigestV1 = "genericBlobDigest/v1"
+
+func init() {
+ cpi.MustRegisterDigester(&defaultDigester{})
+ cpi.SetDefaultDigester(&defaultDigester{})
+}
+
+type defaultDigester struct{}
+
+var _ cpi.BlobDigester = (*defaultDigester)(nil)
+
+func (d defaultDigester) GetType() cpi.DigesterType {
+ return cpi.DigesterType{
+ HashAlgorithm: "",
+ NormalizationAlgorithm: GenericBlobDigestV1,
+ }
+}
+
+func (d defaultDigester) DetermineDigest(typ string, acc cpi.AccessMethod, preferred signing.Hasher) (*cpi.DigestDescriptor, error) {
+ r, err := acc.Reader()
+ if err != nil {
+ return nil, err
+ }
+ hash := preferred.Create()
+
+ if _, err := io.Copy(hash, r); err != nil {
+ return nil, err
+ }
+
+ return &cpi.DigestDescriptor{
+ Value: fmt.Sprintf("%x", hash.Sum(nil)),
+ HashAlgorithm: preferred.Algorithm(),
+ NormalisationAlgorithm: GenericBlobDigestV1,
+ }, nil
+}
diff --git a/api/ocm/extensions/digester/digesters/init.go b/api/ocm/extensions/digester/digesters/init.go
new file mode 100644
index 000000000..b3c0880c4
--- /dev/null
+++ b/api/ocm/extensions/digester/digesters/init.go
@@ -0,0 +1,6 @@
+package digesters
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/digester/digesters/artifact"
+ _ "ocm.software/ocm/api/ocm/extensions/digester/digesters/blob"
+)
diff --git a/api/ocm/extensions/download/config/type.go b/api/ocm/extensions/download/config/type.go
new file mode 100644
index 000000000..29c8a2ca2
--- /dev/null
+++ b/api/ocm/extensions/download/config/type.go
@@ -0,0 +1,91 @@
+package config
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/config"
+ cfgcpi "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ConfigType = "downloader.ocm" + cfgcpi.OCM_CONFIG_TYPE_SUFFIX
+ ConfigTypeV1 = ConfigType + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigType, usage))
+ cfgcpi.RegisterConfigType(cfgcpi.NewConfigType[*Config](ConfigTypeV1, usage))
+}
+
+// Config describes a memory based config interface.
+type Config struct {
+ runtime.ObjectVersionedType `json:",inline"`
+ Registrations []Registration `json:"registrations,omitempty"`
+}
+
+type Registration struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ download.HandlerOptions `json:",inline"`
+ Config download.HandlerConfig `json:"config,omitempty"`
+}
+
+// New creates a new memory ConfigSpec.
+func New() *Config {
+ return &Config{
+ ObjectVersionedType: runtime.NewVersionedObjectType(ConfigType),
+ }
+}
+
+func (a *Config) GetType() string {
+ return ConfigType
+}
+
+func (a *Config) AddRegistration(hdlrs ...Registration) error {
+ for i, h := range hdlrs {
+ if h.Name == "" {
+ return fmt.Errorf("handler registration %d requires a name", i)
+ }
+ }
+ a.Registrations = append(a.Registrations, hdlrs...)
+ return nil
+}
+
+func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
+ t, ok := target.(cpi.Context)
+ if !ok {
+ return config.ErrNoContext(ConfigType)
+ }
+ reg := download.For(t)
+ for _, h := range a.Registrations {
+ accepted, err := reg.RegisterByName(h.Name, t, h.Config, &h.HandlerOptions)
+ if err != nil {
+ return errors.Wrapf(err, "registering download handler %q[%s]", h.Name, h.Description)
+ }
+ if !accepted {
+ download.Logger(t).Info("no matching handler for configuration %q[%s]", h.Name, h.Description)
+ }
+ }
+ return nil
+}
+
+const usage = `
+The config type ` + ConfigType + `
can be used to define a list
+of pre-configured download handler registrations (see + type: ` + ConfigType + ` + descrition: "my standard download handler configuration" + handlers: + - name: oci/artifact + artifactType: ociImage + mimeType: + config: ... + ... ++` diff --git a/pkg/contexts/ocm/download/doc.go b/api/ocm/extensions/download/doc.go similarity index 100% rename from pkg/contexts/ocm/download/doc.go rename to api/ocm/extensions/download/doc.go diff --git a/api/ocm/extensions/download/download.go b/api/ocm/extensions/download/download.go new file mode 100644 index 000000000..36313d761 --- /dev/null +++ b/api/ocm/extensions/download/download.go @@ -0,0 +1,69 @@ +package download + +import ( + "github.com/mandelsoft/goutils/optionutils" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/utils" + common "ocm.software/ocm/api/utils/misc" +) + +type Option = optionutils.Option[*Options] + +type Options struct { + Printer common.Printer + FileSystem vfs.FileSystem +} + +func (o *Options) ApplyTo(opts *Options) { + if o.Printer != nil { + opts.Printer = o.Printer + } + if o.FileSystem != nil { + opts.FileSystem = o.FileSystem + } +} + +//////////////////////////////////////////////////////////////////////////////// + +type filesystem struct { + fs vfs.FileSystem +} + +func (o *filesystem) ApplyTo(opts *Options) { + if o.fs != nil { + opts.FileSystem = o.fs + } +} + +func WithFileSystem(fs vfs.FileSystem) Option { + return &filesystem{fs} +} + +//////////////////////////////////////////////////////////////////////////////// + +type printer struct { + pr common.Printer +} + +func (o *printer) ApplyTo(opts *Options) { + if o.pr != nil { + opts.Printer = o.pr + } +} + +func WithPrinter(pr common.Printer) Option { + return &printer{pr} +} + +//////////////////////////////////////////////////////////////////////////////// + +func DownloadResource(ctx cpi.ContextProvider, r cpi.ResourceAccess, path string, opts ...Option) (string, error) { + eff := optionutils.EvalOptions(opts...) + + fs := utils.FileSystem(eff.FileSystem) + pr := utils.OptionalDefaulted(common.NewPrinter(nil), eff.Printer) + _, tgt, err := For(ctx).Download(pr, r, path, fs) + return tgt, err +} diff --git a/api/ocm/extensions/download/handlers/blob/handler.go b/api/ocm/extensions/download/handlers/blob/handler.go new file mode 100644 index 000000000..af7449bfa --- /dev/null +++ b/api/ocm/extensions/download/handlers/blob/handler.go @@ -0,0 +1,47 @@ +package blob + +import ( + "io" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/download" + common "ocm.software/ocm/api/utils/misc" +) + +type Handler struct{} + +func init() { + download.Register(&Handler{}, download.ForArtifactType(download.ALL)) +} + +func wrapErr(err error, racc cpi.ResourceAccess) error { + if err == nil { + return nil + } + m := racc.Meta() + return errors.Wrapf(err, "resource %s/%s%s", m.GetName(), m.GetVersion(), m.ExtraIdentity.String()) +} + +func (_ Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { + rd, err := cpi.GetResourceReader(racc) + if err != nil { + return true, "", wrapErr(err, racc) + } + defer rd.Close() + if path == "" { + path = racc.Meta().GetName() + } + file, err := fs.OpenFile(path, vfs.O_TRUNC|vfs.O_CREATE|vfs.O_WRONLY, 0o660) + if err != nil { + return true, "", wrapErr(errors.Wrapf(err, "creating target file %q", path), racc) + } + defer file.Close() + n, err := io.Copy(file, rd) + if err == nil { + p.Printf("%s: %d byte(s) written\n", path, n) + } + return true, path, wrapErr(err, racc) +} diff --git a/pkg/contexts/ocm/download/handlers/blueprint/blueprint_test.go b/api/ocm/extensions/download/handlers/blueprint/blueprint_test.go similarity index 77% rename from pkg/contexts/ocm/download/handlers/blueprint/blueprint_test.go rename to api/ocm/extensions/download/handlers/blueprint/blueprint_test.go index 5e98f8f65..952ac0aff 100644 --- a/pkg/contexts/ocm/download/handlers/blueprint/blueprint_test.go +++ b/api/ocm/extensions/download/handlers/blueprint/blueprint_test.go @@ -4,21 +4,21 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/open-component-model/ocm/pkg/env/builder" + . "ocm.software/ocm/api/helper/builder" "github.com/mandelsoft/vfs/pkg/projectionfs" - "github.com/open-component-model/ocm/pkg/common" - "github.com/open-component-model/ocm/pkg/common/accessio" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/testhelper" - "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/ociartifact" - v1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" - "github.com/open-component-model/ocm/pkg/contexts/ocm/download" - "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/blueprint" - ctfocm "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" - tenv "github.com/open-component-model/ocm/pkg/env" - "github.com/open-component-model/ocm/pkg/utils/tarutils" + tenv "ocm.software/ocm/api/helper/env" + "ocm.software/ocm/api/oci/testhelper" + v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" + "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/ocm/extensions/download/handlers/blueprint" + ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/tarutils" ) const ( diff --git a/pkg/contexts/ocm/download/handlers/blueprint/extractor.go b/api/ocm/extensions/download/handlers/blueprint/extractor.go similarity index 85% rename from pkg/contexts/ocm/download/handlers/blueprint/extractor.go rename to api/ocm/extensions/download/handlers/blueprint/extractor.go index 9a90af8b7..481eda4f4 100644 --- a/pkg/contexts/ocm/download/handlers/blueprint/extractor.go +++ b/api/ocm/extensions/download/handlers/blueprint/extractor.go @@ -5,13 +5,13 @@ import ( "github.com/mandelsoft/vfs/pkg/projectionfs" "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/open-component-model/ocm/pkg/blobaccess/blobaccess" - "github.com/open-component-model/ocm/pkg/common" - "github.com/open-component-model/ocm/pkg/common/accessio" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/common/compression" - "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset" - "github.com/open-component-model/ocm/pkg/utils/tarutils" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/compression" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/tarutils" ) const ( diff --git a/api/ocm/extensions/download/handlers/blueprint/handler.go b/api/ocm/extensions/download/handlers/blueprint/handler.go new file mode 100644 index 000000000..f5736d851 --- /dev/null +++ b/api/ocm/extensions/download/handlers/blueprint/handler.go @@ -0,0 +1,85 @@ +package blueprint + +import ( + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/goutils/set" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/ocm/cpi" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + registry "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" + "ocm.software/ocm/api/utils/mime" + common "ocm.software/ocm/api/utils/misc" +) + +const ( + TYPE = resourcetypes.BLUEPRINT + LEGACY_TYPE = resourcetypes.BLUEPRINT_LEGACY + CONFIG_MIME_TYPE = "application/vnd.gardener.landscaper.blueprint.config.v1" +) + +type Extractor func(pr common.Printer, handler *Handler, access blobaccess.DataAccess, path string, fs vfs.FileSystem) (bool, error) + +var ( + supportedArtifactTypes []string + mimeTypeExtractorRegistry map[string]Extractor +) + +type Handler struct { + ociConfigMimeTypes set.Set[string] +} + +func init() { + supportedArtifactTypes = []string{TYPE, LEGACY_TYPE} + mimeTypeExtractorRegistry = map[string]Extractor{ + mime.MIME_TAR: ExtractArchive, + mime.MIME_TGZ: ExtractArchive, + mime.MIME_TGZ_ALT: ExtractArchive, + BLUEPRINT_MIMETYPE: ExtractArchive, + BLUEPRINT_MIMETYPE_COMPRESSED: ExtractArchive, + BLUEPRINT_MIMETYPE_LEGACY: ExtractArchive, + BLUEPRINT_MIMETYPE_LEGACY_COMPRESSED: ExtractArchive, + } + for _, t := range append(artdesc.ToArchiveMediaTypes(artdesc.MediaTypeImageManifest), artdesc.ToArchiveMediaTypes(artdesc.MediaTypeDockerSchema2Manifest)...) { + mimeTypeExtractorRegistry[t] = ExtractArtifact + } + + h := New() + + registry.Register(h, registry.ForArtifactType(TYPE)) + registry.Register(h, registry.ForArtifactType(LEGACY_TYPE)) +} + +func New(configmimetypes ...string) *Handler { + if len(configmimetypes) == 0 || utils.Optional(configmimetypes...) == "" { + configmimetypes = []string{CONFIG_MIME_TYPE} + } + return &Handler{ + ociConfigMimeTypes: set.New[string](configmimetypes...), + } +} + +func (h *Handler) Download(pr common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (_ bool, _ string, err error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagationf(&err, "downloading blueprint") + + meth, err := racc.AccessMethod() + if err != nil { + return false, "", err + } + finalize.Close(meth) + + ex := mimeTypeExtractorRegistry[meth.MimeType()] + if ex == nil { + return false, "", nil + } + + ok, err := ex(pr, h, meth, path, fs) + if err != nil || !ok { + return ok, "", err + } + return true, path, nil +} diff --git a/api/ocm/extensions/download/handlers/blueprint/registration.go b/api/ocm/extensions/download/handlers/blueprint/registration.go new file mode 100644 index 000000000..dfe3315a7 --- /dev/null +++ b/api/ocm/extensions/download/handlers/blueprint/registration.go @@ -0,0 +1,95 @@ +package blueprint + +import ( + "fmt" + "strings" + + "github.com/mandelsoft/goutils/errors" + "golang.org/x/exp/slices" + + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/utils" + "ocm.software/ocm/api/utils/listformat" + "ocm.software/ocm/api/utils/registrations" +) + +const PATH = "landscaper/blueprint" + +func init() { + download.RegisterHandlerRegistrationHandler(PATH, &RegistrationHandler{}) +} + +type Config struct { + OCIConfigTypes []string `json:"ociConfigTypes"` +} + +func AttributeDescription() map[string]string { + return map[string]string{ + "ociConfigTypes": "a list of accepted OCI config archive mime types\n" + + "defaulted by
" + CONFIG_MIME_TYPE + "
.",
+ }
+}
+
+type RegistrationHandler struct{}
+
+var _ download.HandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx download.Target, config download.HandlerConfig, olist ...download.HandlerOption) (bool, error) {
+ var err error
+
+ if handler != "" {
+ return true, fmt.Errorf("invalid blueprint handler %q", handler)
+ }
+
+ opts := download.NewHandlerOptions(olist...)
+ if opts.MimeType != "" && !slices.Contains(supportedArtifactTypes, opts.ArtifactType) {
+ return false, errors.Newf("artifact type %s not supported", opts.ArtifactType)
+ }
+
+ if opts.MimeType != "" {
+ if _, ok := mimeTypeExtractorRegistry[opts.MimeType]; !ok {
+ return false, errors.Newf("mime type %s not supported", opts.MimeType)
+ }
+ }
+
+ attr, err := registrations.DecodeDefaultedConfig[Config](config)
+ if err != nil {
+ return true, errors.Wrapf(err, "cannot unmarshal download handler configuration")
+ }
+
+ h := New(attr.OCIConfigTypes...)
+ if opts.MimeType == "" {
+ for m := range mimeTypeExtractorRegistry {
+ opts.MimeType = m
+ download.For(ctx).Register(h, opts)
+ }
+ } else {
+ download.For(ctx).Register(h, opts)
+ }
+
+ return true, nil
+}
+
+func (r *RegistrationHandler) GetHandlers(ctx cpi.Context) registrations.HandlerInfos {
+ return registrations.NewLeafHandlerInfo("uploading an OCI artifact to an OCI registry", `
+The artifact
downloader is able to transfer OCI artifact-like resources
+into an OCI registry given by the combination of the download target and the
+registration config.
+
+If no config is given, the target must be an OCI reference with a potentially
+omitted repository. The repo part is derived from the reference hint provided
+by the resource's access specification.
+
+If the config is given, the target is used as repository name prefixed with an
+optional repository prefix given by the configuration.
+
+The following artifact media types are supported:
+`+listformat.FormatList("", utils.StringMapKeys(mimeTypeExtractorRegistry)...)+`
+It accepts a config with the following fields:
+`+listformat.FormatMapElements("", AttributeDescription())+`
+
+This handler is by default registered for the following artifact types:
+`+strings.Join(supportedArtifactTypes, ","),
+ )
+}
diff --git a/api/ocm/extensions/download/handlers/blueprint/registration_test.go b/api/ocm/extensions/download/handlers/blueprint/registration_test.go
new file mode 100644
index 000000000..192eb26b7
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/blueprint/registration_test.go
@@ -0,0 +1,78 @@
+package blueprint_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "github.com/mandelsoft/vfs/pkg/projectionfs"
+
+ tenv "ocm.software/ocm/api/helper/env"
+ "ocm.software/ocm/api/oci/testhelper"
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/extensions/download/handlers/blueprint"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/tarutils"
+)
+
+var _ = Describe("blueprint downloader registration", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder(tenv.TestData())
+
+ MustBeSuccessful(tarutils.CreateTarFromFs(Must(projectionfs.New(env, TESTDATA_PATH)), ARCHIVE_PATH, tarutils.Gzip, env))
+
+ env.OCICommonTransport(OCI, accessio.FormatDirectory, func() {
+ env.Namespace(OCINAMESPACE, func() {
+ env.Manifest(OCIVERSION, func() {
+ env.Config(func() {
+ env.BlobStringData(MIMETYPE, "{}")
+ })
+ env.Layer(func() {
+ env.BlobFromFile(MIMETYPE, ARCHIVE_PATH)
+ })
+ })
+ })
+ })
+
+ testhelper.FakeOCIRepo(env, OCI, OCIHOST)
+ env.OCMCommonTransport(CTF, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMPONENT, VERSION, func() {
+ env.Resource(OCI_ARTIFACT_NAME, ARTIFACT_VERSION, ARTIFACT_TYPE, v1.ExternalRelation, func() {
+ env.Access(ociartifact.New(OCIHOST + ".alias/" + OCINAMESPACE + ":" + OCIVERSION))
+ })
+ })
+ })
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ It("register and use blueprint downloader for artifact type \"testartifacttype\"", func() {
+ // As the handler is not registered for the artifact type "testartifacttype" per default (thus, in the
+ // init-function of handler.go), this test fails if the registration does not work.
+ Expect(download.For(env).RegisterByName(blueprint.PATH, env.OCMContext(), &blueprint.Config{[]string{MIMETYPE}}, download.ForArtifactType(ARTIFACT_TYPE))).To(BeTrue())
+
+ repo := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, CTF, 0, env))
+ defer Close(repo)
+ cv := Must(repo.LookupComponentVersion(COMPONENT, VERSION))
+ defer Close(cv)
+ racc := Must(cv.GetResourceByIndex(0))
+
+ p, buf := common.NewBufferedPrinter()
+ ok, path := Must2(download.For(env).Download(p, racc, DOWNLOAD_PATH, env))
+ Expect(ok).To(BeTrue())
+ Expect(path).To(Equal(DOWNLOAD_PATH))
+ Expect(env.FileExists(DOWNLOAD_PATH + "/blueprint.yaml")).To(BeTrue())
+ Expect(env.FileExists(DOWNLOAD_PATH + "/test/README.md")).To(BeTrue())
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(DOWNLOAD_PATH + ": 2 file(s) with 390 byte(s) written"))
+ })
+})
diff --git a/pkg/contexts/ocm/download/handlers/blueprint/suite_test.go b/api/ocm/extensions/download/handlers/blueprint/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/blueprint/suite_test.go
rename to api/ocm/extensions/download/handlers/blueprint/suite_test.go
diff --git a/pkg/contexts/ocm/download/handlers/blueprint/testdata/blueprint/blueprint.yaml b/api/ocm/extensions/download/handlers/blueprint/testdata/blueprint/blueprint.yaml
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/blueprint/testdata/blueprint/blueprint.yaml
rename to api/ocm/extensions/download/handlers/blueprint/testdata/blueprint/blueprint.yaml
diff --git a/pkg/contexts/ocm/download/handlers/blueprint/testdata/blueprint/test/README.md b/api/ocm/extensions/download/handlers/blueprint/testdata/blueprint/test/README.md
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/blueprint/testdata/blueprint/test/README.md
rename to api/ocm/extensions/download/handlers/blueprint/testdata/blueprint/test/README.md
diff --git a/api/ocm/extensions/download/handlers/dirtree/README.md b/api/ocm/extensions/download/handlers/dirtree/README.md
new file mode 100644
index 000000000..fc1db442a
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/dirtree/README.md
@@ -0,0 +1,82 @@
+# Directory Tree Downloader
+
+The standard configuration now provides a downloader for resources of type `directoryTree`and the legacy type `filesystem`.
+
+It acts on the mimetypes for an artifact set (`application/vnd.oci.image.manifest.v1+tar+gzip`) and a tar/tgz archive (`application/x-tar`, `application/x-tar+gzip`, `application/x-tgz`). The default configuration extracts the content to a
+filesystem folder. If the blob format is an artifact set, for example provided by the access method `ociArtifact`,
+the default configuration accepts the image config mimetype (`application/vnd.oci.image.config.v1+json`).
+In this case the final filesystem content provided by the image is downloaded by evaluating the layered image file system.
+
+The default behaviour can just be used with
++import "ocm.software/ocm/api/ocm/extensions/download" +download.For(octx).Download(printer,resourceAccess,targetdir,vfs) ++ +If the resource access describes an appropriate artifact, the new handler is automatically selected. + +As usual, the target is always a virtual filesystem. + +Like all download handlers. the dirtree download handler can also explicitly be used with + +
+import "ocm.software/ocm/api/ocm/extensions/download/handlers/dirtree" +dirtree.New().Download(...) ++ +In this mode, the behaviour can be influenced by specifying any list of accepted OCI artifact config mime types. + +With `New(mimetypes ...string).SetArchiveMode(true)` it is possible to enable the archive target mode. The content is downloaded to an archive instead of extracted filesystem content. + +The handler checks the mimetypes, only, but the default registration is done exclusively for the directory content resource types. +A context can be extended for other resource types with + +``` +download.For(octx),Register(dirtree.New...(...), download.ForCombi(type, [mimetype])) +``` + +The handler also provides additional methods, which can be used to execute more specific tasks, for example +methods for an optimized content access providing an internal virtual filesystem or an archive byte stream trying to avoid unnecessary conversions depending on the actual input format, + +The localization package has been adapted, accordingly. The `localize.Instantiate` method now prefers to use the +download handlers to get to the filesystem content to be configured, instead of expecting an archive resource. +Therefore, any (potentially own) resource type with any format can be used, as long as there is an appropriate downloader configured for the used OCM context. An optional additional parameter can be used to restrict the accepted resource types +to an explicitly given set of types. + +## Use cases + +If you use `dirtree.New(...).Download(...)` you explicitly use the `dirtree` downloader and nothing else. +It only checks the mime types, but not the resource types. So, you can enforce to use it on resources, +regardless of their type to download dirtree-like resources (with a matching mime type). + +If you use `download.For(...).Download(...)` it tries to find a registered downloader with registration +criteria matching the actual resource. This can be used without bothering with the kind of actually used +resource (to just download it, whatever it is in a standard manner). If a matching downloader is found, +it is used, otherwise just the blob is downloaded as provided by the access method. Here, for sure the +`dirtree` downloader is used for the standard scenarios (it is registered for). This is especially the +`directoryTree` resource type with the tar-like mimetypes and the oci artifact archive mime type. But it +is not used for other scenarios. + +So, if you want to use an own resource type (directly expressing the dedicated new meaning of a general +filesystem content), for example `gitOpsTemplate`, which is more expressive than just `directoryTree`. You +can +- either register the `dirtree` handler in advance for your OCM context and for this resource type at the +- registry (then it would automatically be chosen for all downloads using this context) +- or you know what you are doing, and explicitly call the `dirtree` downloader on such a resource. + +If, for example `gitOpsTemplate` should be a standard resource type, we should add such a registration as +part of the standard. + +Another possible scenario, where you might want to use the explicit `dirtree` usage is to overwrite the +standard behaviour for a special use case. For example, an OCI image is typically downloaded as OCI +artifact with the distribution spec format. But. if you want to access the effective filesystem, you +could explicitly use the `dirtree` downloader for an OCI image, which handles this for you. It would +make less sense to use it on a helm chart OCI artifact, because here the layers have a different meaning +than building a directory tree. + +## Registration Handler + +It provides a registration handler with the path `ocm/dirtree`. and a config +object with the fields: +- *`asArchive`* *bool*: download as archive (default is directory tree). +- *`ociConfigtypes`* *[]string*: list of accepted OCI manifest config media types. Default is the OCI image config media type. \ No newline at end of file diff --git a/pkg/contexts/ocm/download/handlers/dirtree/dirtree_test.go b/api/ocm/extensions/download/handlers/dirtree/dirtree_test.go similarity index 92% rename from pkg/contexts/ocm/download/handlers/dirtree/dirtree_test.go rename to api/ocm/extensions/download/handlers/dirtree/dirtree_test.go index eccd219f1..c4818eb29 100644 --- a/pkg/contexts/ocm/download/handlers/dirtree/dirtree_test.go +++ b/api/ocm/extensions/download/handlers/dirtree/dirtree_test.go @@ -11,20 +11,20 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/open-component-model/ocm/pkg/common" - "github.com/open-component-model/ocm/pkg/common/accessio" - "github.com/open-component-model/ocm/pkg/common/accessobj" - "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/artifactset" - "github.com/open-component-model/ocm/pkg/contexts/ocm" - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" - "github.com/open-component-model/ocm/pkg/contexts/ocm/download" - "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/dirtree" - "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf" - "github.com/open-component-model/ocm/pkg/contexts/ocm/resourcetypes" - env2 "github.com/open-component-model/ocm/pkg/env" - "github.com/open-component-model/ocm/pkg/env/builder" - "github.com/open-component-model/ocm/pkg/mime" - "github.com/open-component-model/ocm/pkg/utils/tarutils" + "ocm.software/ocm/api/helper/builder" + env2 "ocm.software/ocm/api/helper/env" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + "ocm.software/ocm/api/ocm" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/ocm/extensions/download/handlers/dirtree" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/mime" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/tarutils" ) const ( diff --git a/api/ocm/extensions/download/handlers/dirtree/handler.go b/api/ocm/extensions/download/handlers/dirtree/handler.go new file mode 100644 index 000000000..06d54d375 --- /dev/null +++ b/api/ocm/extensions/download/handlers/dirtree/handler.go @@ -0,0 +1,364 @@ +package dirtree + +import ( + "fmt" + "io" + "os" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/goutils/set" + "github.com/mandelsoft/vfs/pkg/layerfs" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/projectionfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "golang.org/x/exp/slices" + + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/artdesc" + "ocm.software/ocm/api/oci/extensions/repositories/artifactset" + "ocm.software/ocm/api/ocm/cpi" + resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/accessobj" + "ocm.software/ocm/api/utils/compression" + "ocm.software/ocm/api/utils/iotools" + "ocm.software/ocm/api/utils/mime" + common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/tarutils" +) + +var ( + MimeOCIImageArtifactArchive = artifactset.MediaType(artdesc.MediaTypeImageManifest) + MimeOCIImageArtifact = artdesc.ToContentMediaType(artdesc.MediaTypeImageManifest) +) + +var ( + supportedMimeTypes = []string{MimeOCIImageArtifactArchive, mime.MIME_TGZ, mime.MIME_TGZ_ALT, mime.MIME_TAR} + defaultArtifactTypes = []string{resourcetypes.DIRECTORY_TREE, resourcetypes.FILESYSTEM_LEGACY} +) + +func SupportedMimeTypes() []string { + return slices.Clone(supportedMimeTypes) +} + +type Handler struct { + ociConfigtypes set.Set[string] + archive bool +} + +func New(mimetypes ...string) *Handler { + if len(mimetypes) == 0 || general.Optional(mimetypes...) == "" { + mimetypes = []string{artdesc.MediaTypeImageConfig} + } + return &Handler{ + ociConfigtypes: set.New[string](mimetypes...), + } +} + +var DefaultHandler = New() + +func init() { + for _, t := range defaultArtifactTypes { + for _, m := range supportedMimeTypes { + download.Register(DefaultHandler, download.ForCombi(t, m)) + } + } +} + +func (h *Handler) SetArchiveMode(b bool) *Handler { + h.archive = b + return h +} + +func (h *Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) { + lfs, r, err := h.GetForResource(racc) + if err != nil || (lfs == nil && r == nil) { + return err != nil, "", err + } + if path == "" { + path = racc.Meta().GetName() + } + return h.download(p, fs, path, lfs, r) +} + +func (h *Handler) DownloadFromArtifactSet(pr common.Printer, set *artifactset.ArtifactSet, path string, fs vfs.FileSystem) (bool, string, error) { + lfs, r, err := h.GetForArtifactSet(set) + if err != nil || (lfs == nil && r != nil) { + return err != nil, "", err + } + if path == "" { + path = set.GetMain().String() + } + return h.download(common.NewPrinter(nil), fs, path, lfs, r) +} + +func (h *Handler) download(pr common.Printer, fs vfs.FileSystem, path string, lfs vfs.FileSystem, r io.ReadCloser) (ok bool, dest string, err error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagation(&err) + + if r != nil { + finalize.Close(r) + } + if lfs != nil { + finalize.With(func() error { return vfs.Cleanup(lfs) }) + } + if h.archive { + w, err := fs.OpenFile(path, vfs.O_TRUNC|vfs.O_CREATE|vfs.O_WRONLY, 0o600) + if err != nil { + return true, "", errors.Wrapf(err, "cannot write target archive %s", path) + } + finalize.Close(w) + if r != nil { + n, err := io.Copy(w, r) + if err != nil { + return true, "", errors.Wrapf(err, "cannot copy to archive %s", path) + } + pr.Printf("%s: %d byte(s) written\n", path, n) + return true, path, nil + } else { + cw := iotools.NewCountingWriter(w) + err := tarutils.PackFsIntoTar(lfs, "", cw, tarutils.TarFileSystemOptions{}) + if err == nil { + pr.Printf("%s: %d byte(s) written\n", path, cw.Size()) + } + return true, path, err + } + } else { + err := fs.MkdirAll(path, 0o700) + if err != nil { + return true, "", errors.Wrapf(err, "cannot create target directory") + } + + var fcnt, size int64 + if r != nil { + var p vfs.FileSystem + p, err = projectionfs.New(fs, path) + if err != nil { + return true, "", err + } + fcnt, size, err = tarutils.ExtractTarToFsWithInfo(p, r) + } else { + fcnt, size, err = CopyDir(lfs, "/", fs, path) + } + if err == nil { + pr.Printf("%s: %d file(s) with %d byte(s) written\n", path, fcnt, size) + } + return true, path, err + } +} + +// GetForResource provides a virtual filesystem for an OCi image manifest +// provided by the given resource matching the configured config types. +// It returns nil without error, if the OCI artifact does not match the requirement. +func (h *Handler) GetForResource(racc cpi.ResourceAccess) (fs vfs.FileSystem, reader io.ReadCloser, err error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagation(&err) + + meth, err := racc.AccessMethod() + if err != nil { + return nil, nil, err + } + finalize.Close(meth) + + media := mime.BaseType(meth.MimeType()) + + switch media { + case mime.MIME_TGZ, mime.MIME_TAR: + case MimeOCIImageArtifact: + default: + if !h.ociConfigtypes.Contains(media) && !h.ociConfigtypes.Contains(meth.MimeType()) { + return nil, nil, nil + } + } + + r, err := meth.Reader() + if err != nil { + return nil, nil, err + } + if media != MimeOCIImageArtifact { + r, _, err = compression.AutoDecompress(r) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot determine compression for filesystem blob") + } + return nil, finalize.BindToReader(r), nil + } + finalize.Close(r) + set, err := artifactset.Open(accessobj.ACC_READONLY, "", 0, accessio.Reader(r)) + if err != nil { + return nil, nil, err + } + finalize.Close(set) + return h.getForArtifactSet(&finalize, set) +} + +// GetForArtifactSet provides a virtual filesystem for an OCi image manifest +// provided by the given artifact set matching the configured config types. +// It returns nil without error, if the OCI artifact does not match the requirement. +func (h *Handler) GetForArtifactSet(set *artifactset.ArtifactSet) (fs vfs.FileSystem, reader io.ReadCloser, err error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagation(&err) + + return h.getForArtifactSet(&finalize, set) +} + +func (h *Handler) getForArtifactSet(finalize *finalizer.Finalizer, set *artifactset.ArtifactSet) (fs vfs.FileSystem, reader io.ReadCloser, err error) { + m, err := set.GetArtifact(set.GetMain().String()) + if err != nil { + return nil, nil, err + } + finalize.Close(m) + + return h.getForArtifact(finalize, m) +} + +// GetForArtifact provides a virtual filesystem for an OCi image manifest. +// It returns nil without error, if the OCI artifact does not match the requirement. +func (h *Handler) GetForArtifact(art oci.ArtifactAccess) (fs vfs.FileSystem, reader io.ReadCloser, err error) { + var finalize finalizer.Finalizer + defer finalize.FinalizeWithErrorPropagation(&err) + + return h.getForArtifact(&finalize, art) +} + +func (h *Handler) getForArtifact(finalize *finalizer.Finalizer, m oci.ArtifactAccess) (fs vfs.FileSystem, reader io.ReadCloser, err error) { + if !m.IsManifest() { + return nil, nil, fmt.Errorf("oci artifact is no image manifest") + } + macc := m.ManifestAccess() + if !h.ociConfigtypes.Contains(macc.GetDescriptor().Config.MediaType) { + return nil, nil, nil + } + + var cfs vfs.FileSystem + finalize.With(func() error { + return vfs.Cleanup(cfs) + }) + + // setup layered filesystem from manifest layers + for i, l := range macc.GetDescriptor().Layers { + nested := finalize.Nested() + + blob, err := macc.GetBlob(l.Digest) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot get blob for layer %d", i) + } + nested.Close(blob) + r, err := blob.Reader() + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot get reader for layer blob %d", i) + } + nested.Close(r) + r, _, err = compression.AutoDecompress(r) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot determine compression for layer blob %d", i) + } + + if len(macc.GetDescriptor().Layers) == 1 { + // return archive reader to enable optimized handling bay caller + return nil, finalize.BindToReader(r), nil + } + + nested.Close(r) + + fslayer, err := osfs.NewTempFileSystem() + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot create filesystem for layer %d", i) + } + nested.With(func() error { + return vfs.Cleanup(fslayer) + }) + err = tarutils.ExtractTarToFs(fslayer, r) + if err != nil { + return nil, nil, errors.Wrapf(err, "cannot unpack layer blob %d", i) + } + + if cfs == nil { + cfs = fslayer + } else { + cfs = layerfs.New(fslayer, cfs) + } + fslayer = nil // don't cleanup used layer + if err := nested.Finalize(); err != nil { + return nil, nil, err + } + } + fs = cfs + cfs = nil // don't cleanup used filesystem + return fs, nil, nil +} + +// TODO: to be moved to vfs + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory may exist. +// Symlinks are ignored and skipped. +func CopyDir(srcfs vfs.FileSystem, src string, dstfs vfs.FileSystem, dst string) (int64, int64, error) { + var fcnt, bcnt int64 + var n, b int64 + + src = vfs.Trim(srcfs, src) + dst = vfs.Trim(dstfs, dst) + + si, err := srcfs.Stat(src) + if err != nil { + return 0, 0, err + } + if !si.IsDir() { + return 0, 0, vfs.NewPathError("CopyDir", src, vfs.ErrNotDir) + } + + di, err := dstfs.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return 0, 0, err + } + if err == nil && !di.IsDir() { + return 0, 0, vfs.NewPathError("CopyDir", dst, vfs.ErrNotDir) + } + + err = dstfs.MkdirAll(dst, si.Mode()) + if err != nil { + return 0, 0, err + } + + entries, err := vfs.ReadDir(srcfs, src) + if err != nil { + return 0, 0, err + } + + for _, entry := range entries { + srcPath := vfs.Join(srcfs, src, entry.Name()) + dstPath := vfs.Join(dstfs, dst, entry.Name()) + + if entry.IsDir() { + n, b, err = CopyDir(srcfs, srcPath, dstfs, dstPath) + fcnt += n + bcnt += b + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + var old string + old, err = srcfs.Readlink(srcPath) + if err == nil { + err = dstfs.Symlink(old, dstPath) + } + if err == nil { + fcnt++ + err = os.Chmod(dst, entry.Mode()) + } + } else { + err = vfs.CopyFile(srcfs, srcPath, dstfs, dstPath) + if err == nil { + bcnt += entry.Size() + fcnt++ + } + } + } + if err != nil { + return fcnt, bcnt, err + } + } + return fcnt, bcnt, nil +} diff --git a/api/ocm/extensions/download/handlers/dirtree/registration.go b/api/ocm/extensions/download/handlers/dirtree/registration.go new file mode 100644 index 000000000..5f521b3c9 --- /dev/null +++ b/api/ocm/extensions/download/handlers/dirtree/registration.go @@ -0,0 +1,81 @@ +package dirtree + +import ( + "fmt" + + "github.com/mandelsoft/goutils/errors" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/slices" + + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/extensions/download" + "ocm.software/ocm/api/utils/listformat" + "ocm.software/ocm/api/utils/registrations" +) + +func init() { + download.RegisterHandlerRegistrationHandler("ocm/dirtree", &RegistrationHandler{}) +} + +type Config struct { + AsArchive bool `json:"asArchive"` + OCIConfigTypes []string `json:"ociConfigTypes"` +} + +func AttributeDescription() map[string]string { + return map[string]string{ + "asArchive": "flag to request an archive download", + "ociConfigTypes": "a list of accepted OCI config archive mime types\n" + + "defaulted by
" + ociv1.MediaTypeImageConfig + "
.",
+ }
+}
+
+type RegistrationHandler struct{}
+
+var _ download.HandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx download.Target, config download.HandlerConfig, olist ...download.HandlerOption) (bool, error) {
+ var err error
+
+ if handler != "" {
+ return true, fmt.Errorf("invalid dirtree handler %q", handler)
+ }
+
+ attr, err := registrations.DecodeDefaultedConfig[Config](config)
+ if err != nil {
+ return true, errors.Wrapf(err, "cannot unmarshal download handler configuration")
+ }
+
+ opts := download.NewHandlerOptions(olist...)
+ if opts.MimeType != "" && !slices.Contains(supportedMimeTypes, opts.MimeType) {
+ return true, errors.Wrapf(err, "mime type %s not supported", opts.MimeType)
+ }
+ if opts.ArtifactType != "" && slices.Contains(defaultArtifactTypes, opts.ArtifactType) && !attr.AsArchive {
+ return true, nil
+ }
+
+ h := New(attr.OCIConfigTypes...).SetArchiveMode(attr.AsArchive)
+ if opts.MimeType == "" {
+ for _, m := range supportedMimeTypes {
+ opts.MimeType = m
+ download.For(ctx).Register(h, opts)
+ }
+ } else {
+ download.For(ctx).Register(h, opts)
+ }
+
+ return true, nil
+}
+
+func (r *RegistrationHandler) GetHandlers(ctx cpi.Context) registrations.HandlerInfos {
+ return registrations.NewLeafHandlerInfo("downloading directory tree-like resources", `
+The dirtree
downloader is able to download directory-tree like
+resources as directory structure (default) or archive.
+The following artifact media types are supported:
+`+listformat.FormatList("", SupportedMimeTypes()...)+`
+By default, it is registered for the following resource types:
+`+listformat.FormatList("", defaultArtifactTypes...)+`
+It accepts a config with the following fields:
+`+listformat.FormatMapElements("", AttributeDescription()),
+ )
+}
diff --git a/api/ocm/extensions/download/handlers/dirtree/registration_test.go b/api/ocm/extensions/download/handlers/dirtree/registration_test.go
new file mode 100644
index 000000000..09df184a0
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/dirtree/registration_test.go
@@ -0,0 +1,137 @@
+package dirtree_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/vfs/pkg/projectionfs"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/helper/builder"
+ env2 "ocm.software/ocm/api/helper/env"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/extensions/download/handlers/dirtree"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/tarutils"
+)
+
+const TEST_ARTIFACT = "testArtifact"
+
+var _ = Describe("artifact management", func() {
+ var env *builder.Builder
+
+ BeforeEach(func() {
+ env = builder.NewBuilder(env2.TestData())
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ Context("archive", func() {
+ BeforeEach(func() {
+ MustBeSuccessful(tarutils.CreateTarFromFs(Must(projectionfs.New(env, "testdata/layers/all")), "archive", tarutils.Gzip, env))
+
+ env.OCMCommonTransport("ctf", accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMPONENT, VERSION, func() {
+ env.Resource(RESOURCE, VERSION, TEST_ARTIFACT, metav1.LocalRelation, func() {
+ env.BlobFromFile(artifactset.MediaType(mime.MIME_TGZ_ALT), "archive")
+ })
+ })
+ })
+ })
+
+ It("downloads to dir", func() {
+ Expect(download.For(env).RegisterByName("ocm/dirtree", env.OCMContext(), &dirtree.Config{AsArchive: false}, download.ForArtifactType(TEST_ARTIFACT))).To(BeTrue())
+
+ repo := Must(ctf.Open(ocm.DefaultContext(), accessobj.ACC_READONLY, "ctf", 0, env))
+ defer Close(repo)
+ cv := Must(repo.LookupComponentVersion(COMPONENT, VERSION))
+ defer Close(cv)
+ res := Must(cv.GetResource(metav1.NewIdentity(RESOURCE)))
+
+ p, buf := common.NewBufferedPrinter()
+ accepted, path := Must2(download.For(env).Download(p, res, "result", env))
+ Expect(accepted).To(BeTrue())
+ Expect(path).To(Equal("result"))
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(`
+result: 2 file(s) with 25 byte(s) written
+`))
+
+ data := Must(vfs.ReadFile(env, "result/testfile"))
+ Expect(string(data)).To(StringEqualWithContext("testdata\n"))
+ data = Must(vfs.ReadFile(env, "result/dir/nestedfile"))
+ Expect(string(data)).To(StringEqualWithContext("other test data\n"))
+ })
+
+ It("downloads archive to archive", func() {
+ Expect(download.For(env).RegisterByName("ocm/dirtree", env.OCMContext(), &dirtree.Config{AsArchive: true}, download.ForArtifactType(TEST_ARTIFACT))).To(BeTrue())
+
+ repo := Must(ctf.Open(ocm.DefaultContext(), accessobj.ACC_READONLY, "ctf", 0, env))
+ defer Close(repo)
+ cv := Must(repo.LookupComponentVersion(COMPONENT, VERSION))
+ defer Close(cv)
+ res := Must(cv.GetResource(metav1.NewIdentity(RESOURCE)))
+
+ p, buf := common.NewBufferedPrinter()
+ accepted, path := Must2(download.For(env).Download(p, res, "target", env))
+ Expect(accepted).To(BeTrue())
+ Expect(path).To(Equal("target"))
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(`
+target: 3584 byte(s) written
+`))
+
+ MustBeSuccessful(env.MkdirAll("result", 0o700))
+ resultfs := Must(projectionfs.New(env, "result"))
+ MustBeSuccessful(tarutils.ExtractArchiveToFs(resultfs, "target", env))
+
+ data := Must(vfs.ReadFile(env, "result/testfile"))
+ Expect(string(data)).To(StringEqualWithContext("testdata\n"))
+ data = Must(vfs.ReadFile(env, "result/dir/nestedfile"))
+ Expect(string(data)).To(StringEqualWithContext("other test data\n"))
+ })
+
+ It("downloads archive to archive using config", func() {
+ spec := `
+type: downloader.ocm.config.ocm.software
+registrations:
+- name: ocm/dirtree
+ artifactType: ` + TEST_ARTIFACT + `
+ config:
+ asArchive: true
+`
+ env.ConfigContext().ApplyData([]byte(spec), nil, "manual")
+
+ repo := Must(ctf.Open(ocm.DefaultContext(), accessobj.ACC_READONLY, "ctf", 0, env))
+ defer Close(repo)
+ cv := Must(repo.LookupComponentVersion(COMPONENT, VERSION))
+ defer Close(cv)
+ res := Must(cv.GetResource(metav1.NewIdentity(RESOURCE)))
+
+ p, buf := common.NewBufferedPrinter()
+ accepted, path := Must2(download.For(env).Download(p, res, "target", env))
+ Expect(accepted).To(BeTrue())
+ Expect(path).To(Equal("target"))
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(`
+target: 3584 byte(s) written
+`))
+
+ MustBeSuccessful(env.MkdirAll("result", 0o700))
+ resultfs := Must(projectionfs.New(env, "result"))
+ MustBeSuccessful(tarutils.ExtractArchiveToFs(resultfs, "target", env))
+
+ data := Must(vfs.ReadFile(env, "result/testfile"))
+ Expect(string(data)).To(StringEqualWithContext("testdata\n"))
+ data = Must(vfs.ReadFile(env, "result/dir/nestedfile"))
+ Expect(string(data)).To(StringEqualWithContext("other test data\n"))
+ })
+ })
+})
diff --git a/pkg/contexts/ocm/download/handlers/dirtree/suite_test.go b/api/ocm/extensions/download/handlers/dirtree/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/dirtree/suite_test.go
rename to api/ocm/extensions/download/handlers/dirtree/suite_test.go
diff --git a/pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/0/testfile b/api/ocm/extensions/download/handlers/dirtree/testdata/layers/0/testfile
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/0/testfile
rename to api/ocm/extensions/download/handlers/dirtree/testdata/layers/0/testfile
diff --git a/pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/1/dir/nestedfile b/api/ocm/extensions/download/handlers/dirtree/testdata/layers/1/dir/nestedfile
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/1/dir/nestedfile
rename to api/ocm/extensions/download/handlers/dirtree/testdata/layers/1/dir/nestedfile
diff --git a/pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/all/dir/nestedfile b/api/ocm/extensions/download/handlers/dirtree/testdata/layers/all/dir/nestedfile
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/all/dir/nestedfile
rename to api/ocm/extensions/download/handlers/dirtree/testdata/layers/all/dir/nestedfile
diff --git a/pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/all/testfile b/api/ocm/extensions/download/handlers/dirtree/testdata/layers/all/testfile
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/dirtree/testdata/layers/all/testfile
rename to api/ocm/extensions/download/handlers/dirtree/testdata/layers/all/testfile
diff --git a/api/ocm/extensions/download/handlers/executable/handler.go b/api/ocm/extensions/download/handlers/executable/handler.go
new file mode 100644
index 000000000..8bfe9f147
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/executable/handler.go
@@ -0,0 +1,82 @@
+package executable
+
+import (
+ "io"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/compression"
+ "ocm.software/ocm/api/utils/mime"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+type Handler struct{}
+
+func init() {
+ h := &Handler{}
+ download.Register(h, download.ForCombi(resourcetypes.OCM_PLUGIN, mime.MIME_OCTET))
+ download.Register(h, download.ForCombi(resourcetypes.OCM_PLUGIN, mime.MIME_GZIP))
+ download.Register(h, download.ForCombi(resourcetypes.EXECUTABLE, mime.MIME_OCTET))
+ download.Register(h, download.ForCombi(resourcetypes.EXECUTABLE, mime.MIME_GZIP))
+}
+
+func wrapErr(err error, racc cpi.ResourceAccess) error {
+ if err == nil {
+ return nil
+ }
+ m := racc.Meta()
+ return errors.Wrapf(err, "resource %s/%s%s", m.GetName(), m.GetVersion(), m.ExtraIdentity.String())
+}
+
+func (_ Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) {
+ rd, err := cpi.GetResourceReader(racc)
+ if err != nil {
+ return true, "", wrapErr(err, racc)
+ }
+ defer rd.Close()
+
+ r, _, err := compression.AutoDecompress(rd)
+ if err != nil {
+ return true, "", err
+ }
+ if path == "" {
+ path = racc.Meta().GetName()
+ }
+
+ t := ""
+ if ok, err := vfs.Exists(fs, path); err == nil && ok {
+ t = path
+ path += ".new"
+ }
+ file, err := fs.OpenFile(path, vfs.O_TRUNC|vfs.O_CREATE|vfs.O_WRONLY, 0o660)
+ if err != nil {
+ return true, "", wrapErr(errors.Wrapf(err, "creating target file %q", path), racc)
+ }
+ n, err := io.Copy(file, r)
+ file.Close()
+ if err == nil {
+ if t != "" {
+ err = fs.Remove(t)
+ if err == nil {
+ err = vfs.CopyFile(fs, path, fs, t)
+ }
+ if err == nil {
+ err = fs.Remove(path)
+ }
+ if err == nil {
+ path = t
+ } else {
+ p.Printf("cannot replace existing target file %s -> downloaded to %s\n", t, path)
+ }
+ }
+ p.Printf("%s: %d byte(s) written\n", path, n)
+ fs.Chmod(path, 0o755)
+ } else {
+ fs.Remove(path)
+ }
+ return true, path, wrapErr(err, racc)
+}
diff --git a/api/ocm/extensions/download/handlers/helm/download.go b/api/ocm/extensions/download/handlers/helm/download.go
new file mode 100644
index 000000000..6cbc663e8
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/helm/download.go
@@ -0,0 +1,67 @@
+package helm
+
+import (
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/finalizer"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+func Download(p common.Printer, ctx oci.Context, ref string, path string, fs vfs.FileSystem, creds ...credentials.CredentialsSource) error {
+ _, _, _, err := Download2(p, ctx, ref, path, fs, false, creds...)
+ return err
+}
+
+func Download2(p common.Printer, ctx oci.Context, ref string, path string, fs vfs.FileSystem, asartifact bool, creds ...credentials.CredentialsSource) (chart, prov string, aset string, err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagationf(&err, "downloading helm chart %q", ref)
+
+ r, err := oci.ParseRef(ref)
+ if err != nil {
+ return
+ }
+
+ spec, err := ctx.MapUniformRepositorySpec(&r.UniformRepositorySpec)
+ if err != nil {
+ return
+ }
+
+ repo, err := ctx.RepositoryForSpec(spec, creds...)
+ if err != nil {
+ return
+ }
+ finalize.Close(repo)
+
+ art, err := repo.LookupArtifact(r.Repository, r.Version())
+ if err != nil {
+ return
+ }
+ finalize.Close(art)
+
+ if asartifact {
+ aset = strings.TrimSuffix(path, ".tgz") + ".ctf"
+ ctf, err := artifactset.Open(accessobj.ACC_CREATE|accessobj.ACC_WRITABLE, aset, 0o600, accessio.FormatTGZ, accessio.PathFileSystem(fs))
+ if err != nil {
+ return "", "", "", errors.Wrapf(err, "cannot create artifact set")
+ }
+ err = artifactset.TransferArtifact(art, ctf)
+ if err == nil {
+ ctf.Annotate(artifactset.MAINARTIFACT_ANNOTATION, art.Digest().String())
+ }
+ ctf.Close()
+ if err != nil {
+ fs.Remove(aset)
+ return "", "", "", errors.Wrapf(err, "cannot transfer helm OCI artifact")
+ }
+ }
+ chart, prov, err = download(p, art, path, fs)
+ return chart, prov, aset, err
+}
diff --git a/api/ocm/extensions/download/handlers/helm/handler.go b/api/ocm/extensions/download/handlers/helm/handler.go
new file mode 100644
index 000000000..521e4a465
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/helm/handler.go
@@ -0,0 +1,152 @@
+package helm
+
+import (
+ "io"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/finalizer"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+ helmregistry "helm.sh/helm/v3/pkg/registry"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm/cpi"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ registry "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+ "ocm.software/ocm/api/utils/mime"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const TYPE = resourcetypes.HELM_CHART
+
+type Handler struct{}
+
+func init() {
+ registry.Register(&Handler{}, registry.ForArtifactType(TYPE))
+}
+
+func (h Handler) fromArchive(p common.Printer, meth cpi.AccessMethod, path string, fs vfs.FileSystem) (_ bool, _ string, err error) {
+ basetype := mime.BaseType(helmregistry.ChartLayerMediaType)
+ if mime.BaseType(meth.MimeType()) != basetype {
+ return false, "", nil
+ }
+
+ chart := path
+ if !strings.HasSuffix(chart, ".tgz") {
+ chart += ".tgz"
+ }
+ err = write(p, meth, chart, fs)
+ if err != nil {
+ return true, "", err
+ }
+ return true, chart, nil
+}
+
+func (h Handler) fromOCIArtifact(p common.Printer, meth cpi.AccessMethod, path string, fs vfs.FileSystem) (_ bool, _ string, err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagationf(&err, "from OCI artifact")
+
+ rd, err := meth.Reader()
+ if err != nil {
+ return true, "", err
+ }
+ finalize.Close(rd, "access method reader")
+ set, err := artifactset.Open(accessobj.ACC_READONLY, "", 0, accessio.Reader(rd))
+ if err != nil {
+ return true, "", err
+ }
+ finalize.Close(set, "artifact set")
+ art, err := set.GetArtifact(set.GetMain().String())
+ if err != nil {
+ return true, "", err
+ }
+ finalize.Close(art)
+ chart, _, err := download(p, art, path, fs)
+ if err != nil {
+ return true, "", err
+ }
+ return true, chart, nil
+}
+
+func (h Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (_ bool, _ string, err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagationf(&err, "downloading helm chart")
+
+ if path == "" {
+ path = racc.Meta().GetName()
+ }
+
+ meth, err := racc.AccessMethod()
+ if err != nil {
+ return false, "", err
+ }
+ finalize.Close(meth)
+ if mime.BaseType(meth.MimeType()) != mime.BaseType(artdesc.MediaTypeImageManifest) {
+ return h.fromArchive(p, meth, path, fs)
+ }
+ return h.fromOCIArtifact(p, meth, path, fs)
+}
+
+func download(p common.Printer, art oci.ArtifactAccess, path string, fs vfs.FileSystem) (chart, prov string, err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagation(&err)
+
+ m := art.ManifestAccess()
+ if m == nil {
+ return "", "", errors.Newf("artifact is no image manifest")
+ }
+ if len(m.GetDescriptor().Layers) < 1 {
+ return "", "", errors.Newf("no layers found")
+ }
+ chart = path
+ if !strings.HasSuffix(chart, ".tgz") {
+ chart += ".tgz"
+ }
+ blob, err := m.GetBlob(m.GetDescriptor().Layers[0].Digest)
+ if err != nil {
+ return "", "", err
+ }
+ finalize.Close(blob)
+ err = write(p, blob, chart, fs)
+ if err != nil {
+ return "", "", err
+ }
+ if len(m.GetDescriptor().Layers) > 1 {
+ prov = chart[:len(chart)-3] + "prov"
+ blob, err := m.GetBlob(m.GetDescriptor().Layers[1].Digest)
+ if err != nil {
+ return "", "", err
+ }
+ err = write(p, blob, path, fs)
+ if err != nil {
+ return "", "", err
+ }
+ }
+ return chart, prov, err
+}
+
+func write(p common.Printer, blob blobaccess.DataReader, path string, fs vfs.FileSystem) (err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagation(&err)
+
+ cr, err := blob.Reader()
+ if err != nil {
+ return err
+ }
+ finalize.Close(cr)
+ file, err := fs.OpenFile(path, vfs.O_TRUNC|vfs.O_CREATE|vfs.O_WRONLY, 0o660)
+ if err != nil {
+ return err
+ }
+ finalize.Close(file)
+ n, err := io.Copy(file, cr)
+ if err == nil {
+ p.Printf("%s: %d byte(s) written\n", path, n)
+ }
+ return nil
+}
diff --git a/api/ocm/extensions/download/handlers/init.go b/api/ocm/extensions/download/handlers/init.go
new file mode 100644
index 000000000..6f8614fa4
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/init.go
@@ -0,0 +1,10 @@
+package handlers
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/blob"
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/blueprint"
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/dirtree"
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/executable"
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/helm"
+ _ "ocm.software/ocm/api/ocm/extensions/download/handlers/ocirepo"
+)
diff --git a/api/ocm/extensions/download/handlers/ocirepo/handler.go b/api/ocm/extensions/download/handlers/ocirepo/handler.go
new file mode 100644
index 000000000..f29cacca5
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/ocirepo/handler.go
@@ -0,0 +1,190 @@
+package ocirepo
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/finalizer"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/oci"
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/oci/grammar"
+ "ocm.software/ocm/api/oci/tools/transfer"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/accspeccpi"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/accessobj"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+type handler struct {
+ spec *ociuploadattr.Attribute
+}
+
+func New(repospec ...*ociuploadattr.Attribute) download.Handler {
+ return &handler{spec: general.Optional(repospec...)}
+}
+
+func (h *handler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (accepted bool, target string, err error) {
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagationf(&err, "upload to OCI registry")
+
+ ctx := racc.GetOCMContext()
+ m, err := racc.AccessMethod()
+ if err != nil {
+ return false, "", err
+ }
+ finalize.Close(m, "access method for download")
+
+ mediaType := m.MimeType()
+
+ if !artdesc.IsOCIMediaType(mediaType) || (!strings.HasSuffix(mediaType, "+tar") && !strings.HasSuffix(mediaType, "+tar+gzip")) {
+ return false, "", nil
+ }
+
+ log := download.Logger(ctx).WithName("ocireg")
+
+ var repo oci.Repository
+
+ var version string = "latest"
+
+ aspec := m.AccessSpec()
+ namespace := racc.ReferenceHint()
+ if l, ok := aspec.(*localblob.AccessSpec); namespace == "" && ok {
+ namespace = l.ReferenceName
+ }
+
+ i := strings.LastIndex(namespace, ":")
+ if i > 0 {
+ version = namespace[i:]
+ version = version[1:] // remove colon
+ namespace = namespace[:i]
+ }
+
+ ocictx := ctx.OCIContext()
+
+ var artspec oci.ArtSpec
+ var prefix string
+ var result oci.RefSpec
+
+ if h.spec == nil {
+ log.Debug("no config set")
+ if path == "" {
+ return false, "", fmt.Errorf("path required as target repo specification")
+ }
+ ref, err := oci.ParseRef(path)
+ if err != nil {
+ return true, "", err
+ }
+ result.UniformRepositorySpec = ref.UniformRepositorySpec
+ repospec, err := ocictx.MapUniformRepositorySpec(&ref.UniformRepositorySpec)
+ if err != nil {
+ return true, "", err
+ }
+ repo, err = ocictx.RepositoryForSpec(repospec)
+ if err != nil {
+ return true, "", err
+ }
+ finalize.Close(repo, "repository for downloading OCI artifact")
+ artspec = ref.ArtSpec
+ } else {
+ log.Debug("evaluating config")
+ if path != "" {
+ artspec, err = oci.ParseArt(path)
+ if err != nil {
+ return true, "", err
+ }
+ }
+ var us *oci.UniformRepositorySpec
+ repo, us, prefix, err = h.spec.GetInfo(ctx)
+ if err != nil {
+ return true, "", err
+ }
+ result.UniformRepositorySpec = *us
+ }
+ log.Debug("using artifact spec", "spec", artspec.String())
+ if artspec.Digest != nil {
+ return true, "", fmt.Errorf("digest no possible for target")
+ }
+
+ if artspec.Repository != "" {
+ namespace = artspec.Repository
+ }
+ if artspec.Reference() != "" {
+ version = artspec.Reference()
+ }
+
+ if prefix != "" && namespace != "" {
+ namespace = prefix + grammar.RepositorySeparator + namespace
+ }
+ if version == "" || version == "latest" {
+ version = racc.Meta().GetVersion()
+ }
+ log.Debug("using final target", "namespace", namespace, "version", version)
+ if namespace == "" {
+ return true, "", fmt.Errorf("no OCI namespace")
+ }
+
+ var art oci.ArtifactAccess
+
+ cand := m
+ if local, ok := aspec.(*localblob.AccessSpec); ok {
+ if local.GlobalAccess != nil {
+ s, err := ctx.AccessSpecForSpec(local.GlobalAccess)
+ if err == nil {
+ _ = s
+ // c, err := s.AccessMethod() // TODO: try global access for direct artifact access
+ // set cand to oci access method
+ }
+ }
+ }
+ if ocimeth, ok := accspeccpi.GetAccessMethodImplementation(cand).(ociartifact.AccessMethodImpl); ok {
+ // prepare for optimized point to point implementation
+ art, _, err = ocimeth.GetArtifact()
+ if err != nil {
+ return true, "", errors.Wrapf(err, "cannot access source artifact")
+ }
+ finalize.Close(art)
+ }
+
+ ns, err := repo.LookupNamespace(namespace)
+ if err != nil {
+ return true, "", err
+ }
+ finalize.Close(ns)
+
+ if art == nil {
+ log.Debug("using artifact set transfer mode")
+ set, err := artifactset.OpenFromDataAccess(accessobj.ACC_READONLY, m.MimeType(), m)
+ if err != nil {
+ return true, "", errors.Wrapf(err, "opening resource blob as artifact set")
+ }
+ finalize.Close(set)
+ art, err = set.GetArtifact(set.GetMain().String())
+ if err != nil {
+ return true, "", errors.Wrapf(err, "get artifact from blob")
+ }
+ finalize.Close(art)
+ } else {
+ log.Debug("using direct transfer mode")
+ }
+
+ p.Printf("uploading resource %s to %s[%s:%s]...\n", racc.Meta().GetName(), repo.GetSpecification().UniformRepositorySpec(), namespace, version)
+ err = transfer.TransferArtifact(art, ns, oci.AsTags(version)...)
+ if err != nil {
+ return true, "", errors.Wrapf(err, "transfer artifact")
+ }
+
+ result.Repository = namespace
+ result.Tag = &version
+ return true, result.String(), nil
+}
diff --git a/api/ocm/extensions/download/handlers/ocirepo/registration.go b/api/ocm/extensions/download/handlers/ocirepo/registration.go
new file mode 100644
index 000000000..c5f8c9b2d
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/ocirepo/registration.go
@@ -0,0 +1,87 @@
+package ocirepo
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+ "golang.org/x/exp/slices"
+
+ "ocm.software/ocm/api/oci/artdesc"
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/utils/listformat"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+const PATH = "oci/artifact"
+
+func init() {
+ download.RegisterHandlerRegistrationHandler(PATH, &RegistrationHandler{})
+}
+
+var supportedMimeTypes = []string{
+ artifactset.MediaType(artdesc.MediaTypeImageManifest),
+ artifactset.MediaType(artdesc.MediaTypeImageIndex),
+}
+
+type Config = ociuploadattr.Attribute
+
+func AttributeDescription() map[string]string {
+ return ociuploadattr.AttributeDescription()
+}
+
+type RegistrationHandler struct{}
+
+var _ download.HandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx download.Target, config download.HandlerConfig, olist ...download.HandlerOption) (bool, error) {
+ var err error
+
+ if handler != "" {
+ return true, fmt.Errorf("invalid ocireg handler %q", handler)
+ }
+
+ attr, err := registrations.DecodeConfig[Config](config, ociuploadattr.AttributeType{}.Decode)
+ if err != nil {
+ return true, errors.Wrapf(err, "cannot unmarshal download handler configuration")
+ }
+
+ opts := download.NewHandlerOptions(olist...)
+ if opts.MimeType != "" && !slices.Contains(supportedMimeTypes, opts.MimeType) {
+ return true, errors.Wrapf(err, "mime type %s not supported", opts.MimeType)
+ }
+
+ h := New(attr)
+ if opts.MimeType == "" {
+ for _, m := range supportedMimeTypes {
+ opts.MimeType = m
+ download.For(ctx).Register(h, opts)
+ }
+ } else {
+ download.For(ctx).Register(h, opts)
+ }
+
+ return true, nil
+}
+
+func (r *RegistrationHandler) GetHandlers(ctx cpi.Context) registrations.HandlerInfos {
+ return registrations.NewLeafHandlerInfo("uploading an OCI artifact to an OCI registry", `
+The artifact
downloader is able to transfer OCI artifact-like resources
+into an OCI registry given by the combination of the download target and the
+registration config.
+
+If no config is given, the target must be an OCI reference with a potentially
+omitted repository. The repo part is derived from the reference hint provided
+by the resource's access specification.
+
+If the config is given, the target is used as repository name prefixed with an
+optional repository prefix given by the configuration.
+
+The following artifact media types are supported:
+`+listformat.FormatList("", supportedMimeTypes...)+`
+It accepts a config with the following fields:
+`+listformat.FormatMapElements("", AttributeDescription()),
+ )
+}
diff --git a/pkg/contexts/ocm/download/handlers/ocirepo/suite_test.go b/api/ocm/extensions/download/handlers/ocirepo/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/ocirepo/suite_test.go
rename to api/ocm/extensions/download/handlers/ocirepo/suite_test.go
diff --git a/api/ocm/extensions/download/handlers/ocirepo/upload_test.go b/api/ocm/extensions/download/handlers/ocirepo/upload_test.go
new file mode 100644
index 000000000..1c6bc835b
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/ocirepo/upload_test.go
@@ -0,0 +1,221 @@
+package ocirepo_test
+
+import (
+ "strings"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "ocm.software/ocm/api/oci/extensions/repositories/artifactset"
+ ctfoci "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ "ocm.software/ocm/api/oci/grammar"
+ v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact"
+ resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
+ "ocm.software/ocm/api/ocm/extensions/attrs/ociuploadattr"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/extensions/download/handlers/ocirepo"
+ ctfocm "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ "ocm.software/ocm/api/utils/mime"
+)
+
+const (
+ COMP = "github.com/compa"
+ VERS = "1.0.0"
+ CTF = "ctf"
+)
+
+const (
+ HINT = "ocm.software/test"
+ UPLOAD = "ocm.software/upload"
+)
+
+const (
+ TARGETHOST = "target"
+ TARGETPATH = "/tmp/target"
+)
+
+const (
+ OCIHOST = "source"
+ OCIPATH = "/tmp/source"
+ OCINAMESPACE = "ocm/value"
+ OCIVERSION = "v2.0"
+)
+
+const ARTIFACTSET = "/tmp/set.tgz"
+
+var _ = Describe("upload", func() {
+ var env *Builder
+
+ BeforeEach(func() {
+ env = NewBuilder()
+
+ // fake OCI registry
+ spec := Must(ctfoci.NewRepositorySpec(accessobj.ACC_WRITABLE, TARGETPATH, env))
+ env.OCIContext().SetAlias(TARGETHOST, spec)
+
+ env.OCICommonTransport(TARGETPATH, accessio.FormatDirectory)
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ Context("local blob", func() {
+ BeforeEach(func() {
+ env.ArtifactSet(ARTIFACTSET, accessio.FormatTGZ, func() {
+ env.Manifest(OCIVERSION, func() {
+ env.Config(func() {
+ env.BlobStringData(mime.MIME_JSON, "{}")
+ })
+ env.Layer(func() {
+ env.BlobStringData(mime.MIME_TEXT, "manifestlayer")
+ })
+ })
+ env.Annotation(artifactset.MAINARTIFACT_ANNOTATION, OCIVERSION)
+ })
+
+ env.OCMCommonTransport(CTF, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMP, VERS, func() {
+ env.Provider("mandelsoft")
+ env.Resource("value", "", resourcetypes.OCI_IMAGE, v1.LocalRelation, func() {
+ env.BlobFromFile(artifactset.MediaType(ociv1.MediaTypeImageManifest), ARTIFACTSET)
+ env.Hint(HINT)
+ })
+ })
+ })
+ })
+
+ It("uploads local oci artifact blob", func() {
+ download.For(env).Register(ocirepo.New(), download.ForArtifactType(resourcetypes.OCI_IMAGE))
+
+ src := Must(ctfocm.Open(env, accessobj.ACC_READONLY, CTF, 0, env))
+ defer Close(src, "source ctf")
+
+ cv := Must(src.LookupComponentVersion(COMP, VERS))
+ defer Close(cv)
+
+ racc := Must(cv.GetResourceByIndex(0))
+
+ ok, path := Must2(download.For(env).Download(nil, racc, TARGETHOST+".alias"+grammar.RepositorySeparator+UPLOAD, env))
+ Expect(ok).To(BeTrue())
+ Expect(path).To(Equal("target.alias/ocm.software/upload:1.0.0"))
+
+ env.OCMContext().Finalize()
+
+ target, err := ctfoci.Open(env.OCIContext(), accessobj.ACC_READONLY, TARGETPATH, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ Expect(target.ExistsArtifact(path[strings.Index(path, grammar.RepositorySeparator)+1:strings.Index(path, ":")], VERS)).To(BeTrue())
+ })
+
+ It("uploads local oci artifact blob using named handler", func() {
+ download.RegisterHandlerByName(env, ocirepo.PATH, nil, download.ForArtifactType(resourcetypes.OCI_IMAGE))
+
+ src := Must(ctfocm.Open(env.OCMContext(), accessobj.ACC_READONLY, CTF, 0, accessio.PathFileSystem(env)))
+ defer Close(src, "source ctf")
+
+ cv := Must(src.LookupComponentVersion(COMP, VERS))
+ defer Close(cv)
+
+ racc := Must(cv.GetResourceByIndex(0))
+
+ ok, path := Must2(download.For(env).Download(nil, racc, TARGETHOST+".alias"+grammar.RepositorySeparator+UPLOAD, env))
+ Expect(ok).To(BeTrue())
+ Expect(path).To(Equal("target.alias/ocm.software/upload:1.0.0"))
+
+ env.OCMContext().Finalize()
+
+ target, err := ctfoci.Open(env.OCIContext(), accessobj.ACC_READONLY, TARGETPATH, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ Expect(target.ExistsArtifact(path[strings.Index(path, grammar.RepositorySeparator)+1:strings.Index(path, ":")], VERS)).To(BeTrue())
+ })
+
+ It("uploads local oci artifact blob using named handler and config", func() {
+ cfg := ociuploadattr.Attribute{
+ Ref: TARGETHOST + ".alias" + grammar.RepositorySeparator + "upload",
+ }
+ download.RegisterHandlerByName(env, ocirepo.PATH, cfg, download.ForArtifactType(resourcetypes.OCI_IMAGE))
+
+ src := Must(ctfocm.Open(env.OCMContext(), accessobj.ACC_READONLY, CTF, 0, accessio.PathFileSystem(env)))
+ defer Close(src, "source ctf")
+
+ cv := Must(src.LookupComponentVersion(COMP, VERS))
+ defer Close(cv)
+
+ racc := Must(cv.GetResourceByIndex(0))
+
+ ok, path := Must2(download.For(env).Download(nil, racc, "", env))
+ Expect(ok).To(BeTrue())
+ // Expect(path).To(Equal("CommonTransportFormat::/tmp/target//upload/ocm.software/test:1.0.0"))
+ Expect(path).To(Equal("target.alias/upload/ocm.software/test:1.0.0"))
+
+ env.OCMContext().Finalize()
+
+ target, err := ctfoci.Open(env.OCIContext(), accessobj.ACC_READONLY, TARGETPATH, 0, env)
+ Expect(err).To(Succeed())
+ defer Close(target)
+ // Expect(target.ExistsArtifact(path[strings.Index(path, grammar.RepositorySeparator+grammar.RepositorySeparator)+2:strings.LastIndex(path, ":")], VERS)).To(BeTrue())
+ Expect(target.ExistsArtifact(path[strings.Index(path, grammar.RepositorySeparator)+1:strings.Index(path, ":")], VERS)).To(BeTrue())
+ })
+ })
+
+ Context("oci ref", func() {
+ BeforeEach(func() {
+ env.OCICommonTransport(OCIPATH, accessio.FormatDirectory, func() {
+ env.Namespace(OCINAMESPACE, func() {
+ env.Manifest(OCIVERSION, func() {
+ env.Config(func() {
+ env.BlobStringData(mime.MIME_JSON, "{}")
+ })
+ env.Layer(func() {
+ env.BlobStringData(mime.MIME_TEXT, "manifestlayer")
+ })
+ })
+ })
+ })
+
+ // fake OCI registry
+ spec := Must(ctfoci.NewRepositorySpec(accessobj.ACC_WRITABLE, OCIPATH, env))
+ env.OCIContext().SetAlias(OCIHOST, spec)
+
+ env.OCMCommonTransport(CTF, accessio.FormatDirectory, func() {
+ env.ComponentVersion(COMP, VERS, func() {
+ env.Provider("mandelsoft")
+ env.Resource("value", "", resourcetypes.OCI_IMAGE, v1.LocalRelation, func() {
+ env.Access(ociartifact.New(OCIHOST + ".alias" + grammar.RepositorySeparator + OCINAMESPACE + grammar.TagSeparator + OCIVERSION))
+ })
+ })
+ })
+ })
+
+ It("uploads oci artifact ref", func() {
+ download.For(env).Register(ocirepo.New(), download.ForArtifactType(resourcetypes.OCI_IMAGE))
+
+ src := Must(ctfocm.Open(env.OCMContext(), accessobj.ACC_READONLY, CTF, 0, accessio.PathFileSystem(env)))
+ defer Close(src, "source ctf")
+
+ cv := Must(src.LookupComponentVersion(COMP, VERS))
+ defer Close(cv, "version")
+
+ racc := Must(cv.GetResourceByIndex(0))
+
+ ok, path := Must2(download.For(env).Download(nil, racc, TARGETHOST+".alias"+grammar.RepositorySeparator+UPLOAD, env))
+ Expect(ok).To(BeTrue())
+ Expect(path).To(Equal("target.alias/ocm.software/upload:1.0.0"))
+
+ MustBeSuccessful(env.OCMContext().Finalize())
+
+ target := Must(ctfoci.Open(env.OCIContext(), accessobj.ACC_READONLY, TARGETPATH, 0, env))
+ defer Close(target, "download target")
+ Expect(target.ExistsArtifact(path[strings.Index(path, grammar.RepositorySeparator)+1:strings.Index(path, ":")], VERS)).To(BeTrue())
+ })
+ })
+})
diff --git a/pkg/contexts/ocm/download/handlers/plugin/download_test.go b/api/ocm/extensions/download/handlers/plugin/download_test.go
similarity index 76%
rename from pkg/contexts/ocm/download/handlers/plugin/download_test.go
rename to api/ocm/extensions/download/handlers/plugin/download_test.go
index 79f46a7f2..e2f1dcfe8 100644
--- a/pkg/contexts/ocm/download/handlers/plugin/download_test.go
+++ b/api/ocm/extensions/download/handlers/plugin/download_test.go
@@ -9,23 +9,23 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils"
- . "github.com/open-component-model/ocm/pkg/env/builder"
+ . "ocm.software/ocm/api/helper/builder"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
"github.com/mandelsoft/vfs/pkg/vfs"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/common/accessio"
- "github.com/open-component-model/ocm/pkg/common/accessobj"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/download"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/download/handlers/plugin"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/config"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf"
- "github.com/open-component-model/ocm/pkg/out"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/extensions/download/handlers/plugin"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/plugin/config"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/accessobj"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/out"
+ "ocm.software/ocm/api/utils/runtime"
)
const PLUGIN = "test"
diff --git a/api/ocm/extensions/download/handlers/plugin/handler.go b/api/ocm/extensions/download/handlers/plugin/handler.go
new file mode 100644
index 000000000..5d568aa15
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/plugin/handler.go
@@ -0,0 +1,49 @@
+package plugin
+
+import (
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/finalizer"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/utils/accessio"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+// pluginHandler delegates download format of artifacts to a plugin based handler.
+type pluginHandler struct {
+ plugin plugin.Plugin
+ name string
+ config []byte
+}
+
+func New(p plugin.Plugin, name string, config []byte) (download.Handler, error) {
+ dd := p.GetDownloaderDescriptor(name)
+ if dd == nil {
+ return nil, errors.ErrUnknown(descriptor.KIND_DOWNLOADER, name, p.Name())
+ }
+
+ return &pluginHandler{
+ plugin: p,
+ name: name,
+ config: config,
+ }, nil
+}
+
+func (b *pluginHandler) Download(_ common.Printer, racc cpi.ResourceAccess, path string, _ vfs.FileSystem) (resp bool, eff string, rerr error) {
+ m, err := racc.AccessMethod()
+ if err != nil {
+ return true, "", err
+ }
+ var finalize finalizer.Finalizer
+ defer finalize.FinalizeWithErrorPropagation(&rerr)
+
+ finalize.Close(m, "method for download")
+ r := accessio.NewOndemandReader(m)
+ finalize.Close(r, "reader for downlowd download")
+
+ return b.plugin.Download(b.name, r, racc.Meta().Type, m.MimeType(), path, b.config)
+}
diff --git a/api/ocm/extensions/download/handlers/plugin/registration.go b/api/ocm/extensions/download/handlers/plugin/registration.go
new file mode 100644
index 000000000..3493a71f8
--- /dev/null
+++ b/api/ocm/extensions/download/handlers/plugin/registration.go
@@ -0,0 +1,148 @@
+package plugin
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/ghodss/yaml"
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/xeipuuv/gojsonschema"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr"
+ "ocm.software/ocm/api/ocm/extensions/download"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+type Config = json.RawMessage
+
+func init() {
+ download.RegisterHandlerRegistrationHandler("plugin", &RegistrationHandler{})
+}
+
+type RegistrationHandler struct{}
+
+var _ download.HandlerRegistrationHandler = (*RegistrationHandler)(nil)
+
+func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config download.HandlerConfig, olist ...download.HandlerOption) (bool, error) {
+ path := cpi.NewNamePath(handler)
+
+ if config == nil {
+ return true, fmt.Errorf("target specification required")
+ }
+
+ if len(path) < 1 || len(path) > 2 {
+ return true, fmt.Errorf("plugin handler name must be of the form <plugin name>/<handler>
")
+
+ set := plugincacheattr.Get(ctx)
+ if set == nil {
+ return infos
+ }
+
+ for _, name := range set.PluginNames() {
+ p := set.Get(name)
+ if !p.IsValid() {
+ continue
+ }
+ for _, d := range set.Get(name).GetDescriptor().Downloaders {
+ i := registrations.HandlerInfo{
+ Name: name + "/" + d.GetName(),
+ ShortDesc: "",
+ Description: d.GetDescription(),
+ }
+ infos = append(infos, i)
+ }
+ }
+ return infos
+}
diff --git a/pkg/contexts/ocm/download/handlers/plugin/suite_test.go b/api/ocm/extensions/download/handlers/plugin/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/plugin/suite_test.go
rename to api/ocm/extensions/download/handlers/plugin/suite_test.go
diff --git a/pkg/contexts/ocm/download/handlers/plugin/testdata/test b/api/ocm/extensions/download/handlers/plugin/testdata/test
similarity index 100%
rename from pkg/contexts/ocm/download/handlers/plugin/testdata/test
rename to api/ocm/extensions/download/handlers/plugin/testdata/test
diff --git a/api/ocm/extensions/download/logging.go b/api/ocm/extensions/download/logging.go
new file mode 100644
index 000000000..007cad624
--- /dev/null
+++ b/api/ocm/extensions/download/logging.go
@@ -0,0 +1,13 @@
+package download
+
+import (
+ "github.com/mandelsoft/logging"
+
+ ocmlog "ocm.software/ocm/api/utils/logging"
+)
+
+var REALM = ocmlog.DefineSubRealm("Downloaders", "downloader")
+
+func Logger(ctx logging.ContextProvider, messageContext ...logging.MessageContext) logging.Logger {
+ return ctx.LoggingContext().Logger(append([]logging.MessageContext{REALM}, messageContext...))
+}
diff --git a/api/ocm/extensions/download/registration.go b/api/ocm/extensions/download/registration.go
new file mode 100644
index 000000000..e7f64a08e
--- /dev/null
+++ b/api/ocm/extensions/download/registration.go
@@ -0,0 +1,120 @@
+package download
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/registrations"
+)
+
+type Target = cpi.Context
+
+////////////////////////////////////////////////////////////////////////////////
+
+type HandlerOptions struct {
+ HandlerKey `json:",inline"`
+ Priority int `json:"priority,omitempty"`
+}
+
+func NewHandlerOptions(olist ...HandlerOption) *HandlerOptions {
+ var opts HandlerOptions
+ for _, o := range olist {
+ o.ApplyHandlerOptionTo(&opts)
+ }
+ return &opts
+}
+
+func (o *HandlerOptions) ApplyHandlerOptionTo(opts *HandlerOptions) {
+ if o.Priority > 0 {
+ opts.Priority = o.Priority
+ }
+ o.HandlerKey.ApplyHandlerOptionTo(opts)
+}
+
+type HandlerOption interface {
+ ApplyHandlerOptionTo(*HandlerOptions)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// HandlerKey is the registration key for download handlers.
+type HandlerKey struct {
+ ArtifactType string `json:"artifactType,omitempty"`
+ MimeType string `json:"mimeType,omitempty"`
+}
+
+var _ HandlerOption = HandlerKey{}
+
+func NewHandlerKey(artifactType, mimetype string) HandlerKey {
+ return HandlerKey{
+ ArtifactType: artifactType,
+ MimeType: mimetype,
+ }
+}
+
+func (k HandlerKey) ApplyHandlerOptionTo(opts *HandlerOptions) {
+ if k.ArtifactType != "" {
+ opts.ArtifactType = k.ArtifactType
+ }
+ if k.MimeType != "" {
+ opts.MimeType = k.MimeType
+ }
+}
+
+func ForCombi(artifacttype string, mimetype string) HandlerOption {
+ return HandlerKey{ArtifactType: artifacttype, MimeType: mimetype}
+}
+
+func ForMimeType(mimetype string) HandlerOption {
+ return HandlerKey{MimeType: mimetype}
+}
+
+func ForArtifactType(artifacttype string) HandlerOption {
+ return HandlerKey{ArtifactType: artifacttype}
+}
+
+type prio struct {
+ prio int
+}
+
+func WithPrio(p int) HandlerOption {
+ return prio{p}
+}
+
+func (o prio) ApplyHandlerOptionTo(opts *HandlerOptions) {
+ opts.Priority = o.prio
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type (
+ HandlerConfig = registrations.HandlerConfig
+ HandlerRegistrationHandler = registrations.HandlerRegistrationHandler[Target, HandlerOption]
+ HandlerRegistrationRegistry = registrations.HandlerRegistrationRegistry[Target, HandlerOption]
+
+ RegistrationHandlerInfo = registrations.RegistrationHandlerInfo[Target, HandlerOption]
+)
+
+func NewHandlerRegistrationRegistry(base ...HandlerRegistrationRegistry) HandlerRegistrationRegistry {
+ return registrations.NewHandlerRegistrationRegistry[Target, HandlerOption](base...)
+}
+
+func NewRegistrationHandlerInfo(path string, handler HandlerRegistrationHandler) *RegistrationHandlerInfo {
+ return registrations.NewRegistrationHandlerInfo[Target, HandlerOption](path, handler)
+}
+
+func RegisterHandlerRegistrationHandler(path string, handler HandlerRegistrationHandler) {
+ DefaultRegistry.RegisterRegistrationHandler(path, handler)
+}
+
+func RegisterHandlerByName(ctx cpi.ContextProvider, name string, config HandlerConfig, opts ...HandlerOption) error {
+ hdlrs := For(ctx)
+ o, err := hdlrs.RegisterByName(name, ctx.OCMContext(), config, opts...)
+ if err != nil {
+ return err
+ }
+ if !o {
+ return fmt.Errorf("no matching handler found for %q", name)
+ }
+ return nil
+}
diff --git a/api/ocm/extensions/download/registry.go b/api/ocm/extensions/download/registry.go
new file mode 100644
index 000000000..88264c6d5
--- /dev/null
+++ b/api/ocm/extensions/download/registry.go
@@ -0,0 +1,190 @@
+package download
+
+import (
+ "sort"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/mandelsoft/vfs/pkg/vfs"
+
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/ocmutils/registry"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/registrations"
+ "ocm.software/ocm/api/utils/runtimefinalizer"
+)
+
+const ALL = "*"
+
+type Handler interface {
+ Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error)
+}
+
+const DEFAULT_BLOBHANDLER_PRIO = 100
+
+type PrioHandler struct {
+ Handler
+ Prio int
+}
+
+// MultiHandler is a Handler consisting of a sequence of handlers.
+type MultiHandler []Handler
+
+var _ sort.Interface = MultiHandler(nil)
+
+func (m MultiHandler) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) {
+ errs := errors.ErrListf("download")
+ for _, h := range m {
+ ok, p, err := h.Download(p, racc, path, fs)
+ if ok {
+ return ok, p, err
+ }
+ errs.Add(err)
+ }
+ return false, "", errs.Result()
+}
+
+func (m MultiHandler) Len() int {
+ return len(m)
+}
+
+func (m MultiHandler) Less(i, j int) bool {
+ pi := DEFAULT_BLOBHANDLER_PRIO
+ pj := DEFAULT_BLOBHANDLER_PRIO
+
+ if p, ok := m[i].(*PrioHandler); ok {
+ pi = p.Prio
+ }
+ if p, ok := m[j].(*PrioHandler); ok {
+ pj = p.Prio
+ }
+ return pi > pj
+}
+
+func (m MultiHandler) Swap(i, j int) {
+ m[i], m[j] = m[j], m[i]
+}
+
+type Registry interface {
+ Copy() Registry
+ AsHandlerRegistrationRegistry() registrations.HandlerRegistrationRegistry[Target, HandlerOption]
+
+ registrations.HandlerRegistrationRegistryAccess[Target, HandlerOption]
+
+ Register(hdlr Handler, olist ...HandlerOption)
+ LookupHandler(art, media string) MultiHandler
+ Handler
+ DownloadAsBlob(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error)
+}
+
+func AsHandlerRegistrationRegistry(r Registry) registrations.HandlerRegistrationRegistry[Target, HandlerOption] {
+ if r == nil {
+ return nil
+ }
+ return r.AsHandlerRegistrationRegistry()
+}
+
+type _registry struct {
+ registrations.HandlerRegistrationRegistry[Target, HandlerOption]
+
+ id runtimefinalizer.ObjectIdentity
+ lock sync.RWMutex
+ base Registry
+ handlers *registry.Registry[Handler, registry.RegistrationKey]
+}
+
+func NewRegistry(base ...Registry) Registry {
+ b := general.Optional(base...)
+ return &_registry{
+ id: runtimefinalizer.NewObjectIdentity("downloader.registry.ocm.software"),
+ base: b,
+ HandlerRegistrationRegistry: NewHandlerRegistrationRegistry(AsHandlerRegistrationRegistry(b)),
+ handlers: registry.NewRegistry[Handler, registry.RegistrationKey](),
+ }
+}
+
+func (r *_registry) AsHandlerRegistrationRegistry() registrations.HandlerRegistrationRegistry[Target, HandlerOption] {
+ return r.HandlerRegistrationRegistry
+}
+
+func (r *_registry) Copy() Registry {
+ n := NewRegistry(r.base).(*_registry)
+ n.handlers = r.handlers.Copy()
+ return n
+}
+
+func (r *_registry) LookupHandler(art, media string) MultiHandler {
+ r.lock.RLock()
+ defer r.lock.RUnlock()
+
+ return r.getHandlers(art, media)
+}
+
+func (r *_registry) Register(hdlr Handler, olist ...HandlerOption) {
+ opts := NewHandlerOptions(olist...)
+ r.lock.Lock()
+ defer r.lock.Unlock()
+ if opts.Priority != 0 {
+ hdlr = &PrioHandler{hdlr, opts.Priority}
+ }
+ r.handlers.Register(registry.RegistrationKey{opts.ArtifactType, opts.MimeType}, hdlr)
+}
+
+func (r *_registry) getHandlers(arttype, mediatype string) MultiHandler {
+ list := r.handlers.LookupHandler(registry.RegistrationKey{arttype, mediatype})
+ if r.base != nil {
+ list = append(list, r.base.LookupHandler(arttype, mediatype)...)
+ }
+ return list
+}
+
+func (r *_registry) Download(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) {
+ p = common.AssurePrinter(p)
+ art := racc.Meta().GetType()
+ m, err := racc.AccessMethod()
+ if err != nil {
+ return false, "", err
+ }
+ defer m.Close()
+ mime := m.MimeType()
+ if ok, p, err := r.download(r.LookupHandler(art, mime), p, racc, path, fs); ok {
+ return ok, p, err
+ }
+ return r.download(r.LookupHandler(ALL, ""), p, racc, path, fs)
+}
+
+func (r *_registry) DownloadAsBlob(p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) {
+ return r.download(r.LookupHandler(ALL, ""), p, racc, path, fs)
+}
+
+func (r *_registry) download(list MultiHandler, p common.Printer, racc cpi.ResourceAccess, path string, fs vfs.FileSystem) (bool, string, error) {
+ sort.Stable(list)
+ return list.Download(p, racc, path, fs)
+}
+
+var DefaultRegistry = NewRegistry()
+
+func Register(hdlr Handler, olist ...HandlerOption) {
+ DefaultRegistry.Register(hdlr, olist...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+const ATTR_DOWNLOADER_HANDLERS = "ocm.software/ocm/api/ocm/extensions/download"
+
+func For(ctx cpi.ContextProvider) Registry {
+ if ctx == nil {
+ return DefaultRegistry
+ }
+ return ctx.OCMContext().GetAttributes().GetOrCreateAttribute(ATTR_DOWNLOADER_HANDLERS, create).(Registry)
+}
+
+func create(datacontext.Context) interface{} {
+ return NewRegistry(DefaultRegistry)
+}
+
+func SetFor(ctx datacontext.Context, registry Registry) {
+ ctx.GetAttributes().SetAttribute(ATTR_DOWNLOADER_HANDLERS, registry)
+}
diff --git a/api/ocm/extensions/download/setup.go b/api/ocm/extensions/download/setup.go
new file mode 100644
index 000000000..122f87ea1
--- /dev/null
+++ b/api/ocm/extensions/download/setup.go
@@ -0,0 +1,27 @@
+package download
+
+import (
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+func init() {
+ datacontext.RegisterSetupHandler(datacontext.SetupHandlerFunction(setupContext))
+}
+
+func setupContext(mode datacontext.BuilderMode, ctx datacontext.Context) {
+ if octx, ok := ctx.(cpi.Context); ok {
+ switch mode {
+ case datacontext.MODE_SHARED:
+ fallthrough
+ case datacontext.MODE_DEFAULTED:
+ // do nothing, fallback to the default attribute lookup
+ case datacontext.MODE_EXTENDED:
+ SetFor(octx, NewRegistry(DefaultRegistry))
+ case datacontext.MODE_CONFIGURED:
+ SetFor(octx, DefaultRegistry.Copy())
+ case datacontext.MODE_INITIAL:
+ SetFor(octx, NewRegistry())
+ }
+ }
+}
diff --git a/api/ocm/extensions/labels/init.go b/api/ocm/extensions/labels/init.go
new file mode 100644
index 000000000..26d99bbc1
--- /dev/null
+++ b/api/ocm/extensions/labels/init.go
@@ -0,0 +1,5 @@
+package labels
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types"
+)
diff --git a/api/ocm/extensions/labels/routingslip/entry.go b/api/ocm/extensions/labels/routingslip/entry.go
new file mode 100644
index 000000000..69cf1d05a
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/entry.go
@@ -0,0 +1,93 @@
+package routingslip
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/opencontainers/go-digest"
+
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/internal"
+ "ocm.software/ocm/api/tech/signing"
+ "ocm.software/ocm/api/tech/signing/norm/jcs"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+func AsGenericEntry(u *runtime.UnstructuredTypedObject) *GenericEntry {
+ return internal.AsGenericEntry(u)
+}
+
+func ToGenericEntry(e Entry) (*GenericEntry, error) {
+ return internal.ToGenericEntry(e)
+}
+
+func NewGenericEntryWith(typ string, attrs ...interface{}) (*GenericEntry, error) {
+ r := map[string]interface{}{}
+ i := 0
+ for len(attrs) > i {
+ n, ok := attrs[i].(string)
+ if !ok {
+ return nil, errors.ErrInvalid("key type", fmt.Sprintf("%T", attrs[i]))
+ }
+ r[n] = attrs[i+1]
+ i += 2
+ }
+ return NewGenericEntry(typ, r)
+}
+
+func NewGenericEntry(typ string, data interface{}) (*GenericEntry, error) {
+ u, err := runtime.ToUnstructuredTypedObject(data)
+ if err != nil {
+ return nil, err
+ }
+ if typ != "" {
+ u.SetType(typ)
+ }
+ return AsGenericEntry(u), nil
+}
+
+var excludes = signing.MapExcludes{
+ "digest": nil,
+ "signature": nil,
+}
+
+type HistoryEntries = []HistoryEntry
+
+type HistoryEntry struct {
+ Payload *GenericEntry `json:"payload"`
+ Timestamp metav1.Timestamp `json:"timestamp"`
+ Parent *digest.Digest `json:"parent,omitempty"`
+ Links []Link `json:"links,omitempty"`
+ Digest digest.Digest `json:"digest"`
+ Signature *metav1.SignatureSpec `json:"signature,omitempty"`
+}
+
+func (e *HistoryEntry) Normalize() ([]byte, error) {
+ return signing.Normalize(jcs.New(), e, excludes)
+}
+
+func (e *HistoryEntry) CalculateDigest() (digest.Digest, error) {
+ data, err := e.Normalize()
+ if err != nil {
+ return "", err
+ }
+ return digest.SHA256.FromBytes(data), nil
+}
+
+type Link struct {
+ Name string `json:"name"`
+ Digest digest.Digest `json:"digest"`
+}
+
+func (l Link) Compare(o Link) int {
+ r := strings.Compare(l.Name, o.Name)
+ if r == 0 {
+ r = strings.Compare(l.Digest.String(), o.Digest.String())
+ }
+ return r
+}
+
+func CreateEntry(t runtime.VersionedTypedObject) (Entry, error) {
+ return internal.CreateEntry(t)
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/entrytypes_test.go b/api/ocm/extensions/labels/routingslip/entrytypes_test.go
similarity index 78%
rename from pkg/contexts/ocm/labels/routingslip/entrytypes_test.go
rename to api/ocm/extensions/labels/routingslip/entrytypes_test.go
index fb5cd0821..0cd591b33 100644
--- a/pkg/contexts/ocm/labels/routingslip/entrytypes_test.go
+++ b/api/ocm/extensions/labels/routingslip/entrytypes_test.go
@@ -8,12 +8,12 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip/internal"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip/types/comment"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/ocm"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/internal"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types/comment"
+ "ocm.software/ocm/api/utils/runtime"
)
const TYPE = "my"
diff --git a/api/ocm/extensions/labels/routingslip/init.go b/api/ocm/extensions/labels/routingslip/init.go
new file mode 100644
index 000000000..4dd3cba4f
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/init.go
@@ -0,0 +1,5 @@
+package routingslip
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types"
+)
diff --git a/api/ocm/extensions/labels/routingslip/interface.go b/api/ocm/extensions/labels/routingslip/interface.go
new file mode 100644
index 000000000..dc4f8b0bd
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/interface.go
@@ -0,0 +1,26 @@
+package routingslip
+
+import (
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/internal"
+)
+
+type (
+ Context = internal.Context
+ ContextProvider = ocm.ContextProvider
+ EntryTypeScheme = internal.EntryTypeScheme
+ Entry = internal.Entry
+ GenericEntry = internal.GenericEntry
+)
+
+type SlipAccess interface {
+ Get(name string) (*RoutingSlip, error)
+}
+
+func DefaultEntryTypeScheme() EntryTypeScheme {
+ return internal.DefaultEntryTypeScheme()
+}
+
+func For(ctx ContextProvider) EntryTypeScheme {
+ return internal.For(ctx)
+}
diff --git a/api/ocm/extensions/labels/routingslip/internal/attr.go b/api/ocm/extensions/labels/routingslip/internal/attr.go
new file mode 100644
index 000000000..de61f894c
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/internal/attr.go
@@ -0,0 +1,25 @@
+package internal
+
+import (
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+const ATTR_ROUTINGSLIP_ENTRYTYPES = "ocm.software/ocm/api/ocm/extensions/labels/routingslip"
+
+func For(ctx cpi.ContextProvider) EntryTypeScheme {
+ if ctx == nil {
+ return DefaultEntryTypeScheme()
+ }
+ return ctx.OCMContext().GetAttributes().GetOrCreateAttribute(ATTR_ROUTINGSLIP_ENTRYTYPES, create).(EntryTypeScheme)
+}
+
+func create(datacontext.Context) interface{} {
+ return NewEntryTypeScheme(DefaultEntryTypeScheme())
+}
+
+func SetFor(ctx datacontext.Context, registry EntryTypeScheme) {
+ ctx.GetAttributes().SetAttribute(ATTR_ROUTINGSLIP_ENTRYTYPES, registry)
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/internal/entrytypes.go b/api/ocm/extensions/labels/routingslip/internal/entrytypes.go
similarity index 94%
rename from pkg/contexts/ocm/labels/routingslip/internal/entrytypes.go
rename to api/ocm/extensions/labels/routingslip/internal/entrytypes.go
index 319d2bc61..dbb7f4205 100644
--- a/pkg/contexts/ocm/labels/routingslip/internal/entrytypes.go
+++ b/api/ocm/extensions/labels/routingslip/internal/entrytypes.go
@@ -12,11 +12,11 @@ import (
"github.com/mandelsoft/goutils/sliceutils"
"github.com/modern-go/reflect2"
- "github.com/open-component-model/ocm/pkg/cobrautils/flagsets"
- "github.com/open-component-model/ocm/pkg/cobrautils/flagsets/flagsetscheme"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/runtime"
- "github.com/open-component-model/ocm/pkg/utils"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme"
+ "ocm.software/ocm/api/utils/runtime"
)
type Context = cpi.Context
diff --git a/api/ocm/extensions/labels/routingslip/label.go b/api/ocm/extensions/labels/routingslip/label.go
new file mode 100644
index 000000000..0bce14944
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/label.go
@@ -0,0 +1,109 @@
+package routingslip
+
+import (
+ "sort"
+
+ "github.com/opencontainers/go-digest"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/valuemergehandler/handlers/maplistmerge"
+ "ocm.software/ocm/api/ocm/valuemergehandler/handlers/simplemapmerge"
+ "ocm.software/ocm/api/ocm/valuemergehandler/hpi"
+ "ocm.software/ocm/api/utils"
+)
+
+const NAME = "routing-slips"
+
+type LabelValue map[string]HistoryEntries
+
+var spec = utils.Must(hpi.NewSpecification(
+ simplemapmerge.ALGORITHM,
+ simplemapmerge.NewConfig(
+ "",
+ utils.Must(hpi.NewSpecification(
+ maplistmerge.ALGORITHM,
+ maplistmerge.NewConfig("digest", maplistmerge.MODE_INBOUND),
+ )),
+ )),
+)
+
+func init() {
+ hpi.Assign(hpi.LabelHint(NAME), spec)
+}
+
+func (l LabelValue) Has(name string) bool {
+ return l[name] != nil
+}
+
+func (l LabelValue) Get(name string) (*RoutingSlip, error) {
+ return NewRoutingSlip(name, l)
+}
+
+func (l LabelValue) Query(name string) (*RoutingSlip, error) {
+ a := l[name]
+ if a == nil {
+ return nil, nil
+ }
+ return l.Get(name)
+}
+
+func (l LabelValue) Leaves() []Link {
+ var links []Link
+
+ for k := range l {
+ s, err := l.Get(k)
+ if err == nil {
+ for _, d := range s.Leaves() {
+ links = append(links, Link{
+ Name: k,
+ Digest: d,
+ })
+ }
+ }
+ }
+ sort.Slice(links, func(i, j int) bool { return links[i].Compare(links[j]) < 0 })
+ return links
+}
+
+func (l LabelValue) Set(slip *RoutingSlip) {
+ l[slip.name] = slip.entries
+}
+
+func AddEntry(cv cpi.ComponentVersionAccess, name string, algo string, e Entry, links []Link, parent ...digest.Digest) (*HistoryEntry, error) {
+ var label LabelValue
+ _, err := cv.GetDescriptor().Labels.GetValue(NAME, &label)
+ if err != nil {
+ return nil, err
+ }
+ if label == nil {
+ label = LabelValue{}
+ }
+ slip, err := label.Get(name)
+ if err != nil {
+ return nil, err
+ }
+ entry, err := slip.Add(cv.GetContext(), name, algo, e, links, parent...)
+ if err != nil {
+ return nil, err
+ }
+ label.Set(slip)
+
+ err = Set(cv, label)
+ if err != nil {
+ return nil, err
+ }
+ return entry, nil
+}
+
+func Get(cv cpi.ComponentVersionAccess) (LabelValue, error) {
+ var label LabelValue
+ _, err := cv.GetDescriptor().Labels.GetValue(NAME, &label)
+ if err != nil {
+ return nil, err
+ }
+ return label, nil
+}
+
+func Set(cv cpi.ComponentVersionAccess, label LabelValue) error {
+ return cv.GetDescriptor().Labels.SetValue(NAME, label)
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/slip.go b/api/ocm/extensions/labels/routingslip/slip.go
similarity index 94%
rename from pkg/contexts/ocm/labels/routingslip/slip.go
rename to api/ocm/extensions/labels/routingslip/slip.go
index cceb6a24b..c3ea45d7f 100644
--- a/pkg/contexts/ocm/labels/routingslip/slip.go
+++ b/api/ocm/extensions/labels/routingslip/slip.go
@@ -9,13 +9,13 @@ import (
"github.com/opencontainers/go-digest"
"golang.org/x/exp/slices"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/signingattr"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/signing"
- "github.com/open-component-model/ocm/pkg/signing/hasher/sha256"
- "github.com/open-component-model/ocm/pkg/signing/signutils"
+ "ocm.software/ocm/api/ocm/compdesc"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/attrs/signingattr"
+ "ocm.software/ocm/api/tech/signing"
+ "ocm.software/ocm/api/tech/signing/hasher/sha256"
+ "ocm.software/ocm/api/tech/signing/signutils"
)
const (
diff --git a/pkg/contexts/ocm/labels/routingslip/slip_test.go b/api/ocm/extensions/labels/routingslip/slip_test.go
similarity index 86%
rename from pkg/contexts/ocm/labels/routingslip/slip_test.go
rename to api/ocm/extensions/labels/routingslip/slip_test.go
index c9ceb1882..11f9dd2f4 100644
--- a/pkg/contexts/ocm/labels/routingslip/slip_test.go
+++ b/api/ocm/extensions/labels/routingslip/slip_test.go
@@ -11,11 +11,11 @@ import (
"github.com/opencontainers/go-digest"
"sigs.k8s.io/yaml"
- metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip/types/comment"
- "github.com/open-component-model/ocm/pkg/env/builder"
- "github.com/open-component-model/ocm/pkg/signing/handlers/rsa"
+ "ocm.software/ocm/api/helper/builder"
+ metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types/comment"
+ "ocm.software/ocm/api/tech/signing/handlers/rsa"
)
const (
diff --git a/api/ocm/extensions/labels/routingslip/spi/entrytype_options.go b/api/ocm/extensions/labels/routingslip/spi/entrytype_options.go
new file mode 100644
index 000000000..6edbbc088
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/spi/entrytype_options.go
@@ -0,0 +1,20 @@
+package spi
+
+import (
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme"
+)
+
+type EntryTypeOption = flagsetscheme.TypeOption
+
+func WithFormatSpec(value string) EntryTypeOption {
+ return flagsetscheme.WithFormatSpec(value)
+}
+
+func WithDescription(value string) EntryTypeOption {
+ return flagsetscheme.WithDescription(value)
+}
+
+func WithConfigHandler(value flagsets.ConfigOptionTypeSetHandler) EntryTypeOption {
+ return flagsetscheme.WithConfigHandler(value)
+}
diff --git a/api/ocm/extensions/labels/routingslip/spi/interface.go b/api/ocm/extensions/labels/routingslip/spi/interface.go
new file mode 100644
index 000000000..e3393e46e
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/spi/interface.go
@@ -0,0 +1,28 @@
+package spi
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type (
+ Context = cpi.Context
+ Entry = internal.Entry
+ UnknownEntry = internal.UnknownEntry
+ GenericEntry = internal.GenericEntry
+ EntryType = internal.EntryType
+ EntryTypeScheme = internal.EntryTypeScheme
+)
+
+func NewStrictEntryTypeScheme() runtime.VersionedTypeRegistry[Entry, EntryType] {
+ return internal.NewStrictEntryTypeScheme()
+}
+
+func DefaultEntryTypeScheme() EntryTypeScheme {
+ return internal.DefaultEntryTypeScheme()
+}
+
+func For(ctx cpi.ContextProvider) EntryTypeScheme {
+ return internal.For(ctx)
+}
diff --git a/api/ocm/extensions/labels/routingslip/spi/support.go b/api/ocm/extensions/labels/routingslip/spi/support.go
new file mode 100644
index 000000000..b0e50ca24
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/spi/support.go
@@ -0,0 +1,48 @@
+package spi
+
+import (
+ "ocm.software/ocm/api/utils/cobrautils/flagsets/flagsetscheme"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type EntryTypeVersionScheme = runtime.TypeVersionScheme[Entry, EntryType]
+
+func NewEntryTypeVersionScheme(kind string) EntryTypeVersionScheme {
+ return runtime.NewTypeVersionScheme[Entry, EntryType](kind, NewStrictEntryTypeScheme())
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type EntryFormatVersionRegistry = runtime.FormatVersionRegistry[Entry]
+
+func NewEntryFormatVersionRegistry() EntryFormatVersionRegistry {
+ return runtime.NewFormatVersionRegistry[Entry]()
+}
+
+func MustNewEntryMultiFormatVersion(kind string, formats EntryFormatVersionRegistry) runtime.FormatVersion[Entry] {
+ return runtime.MustNewMultiFormatVersion[Entry](kind, formats)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func NewEntryType[I Entry](name string, opts ...EntryTypeOption) EntryType {
+ return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectType[Entry, I](name), opts...)
+}
+
+func NewEntryTypeByConverter[I Entry, V runtime.VersionedTypedObject](name string, converter runtime.Converter[I, V], opts ...EntryTypeOption) EntryType {
+ return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByConverter[Entry, I, V](name, converter), opts...)
+}
+
+func NewEntryTypeByFormatVersion(name string, fmt runtime.FormatVersion[Entry], opts ...EntryTypeOption) EntryType {
+ return flagsetscheme.NewTypedObjectTypeObject[Entry](runtime.NewVersionedTypedObjectTypeByFormatVersion[Entry](name, fmt), opts...)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+func Register(atype EntryType) {
+ DefaultEntryTypeScheme().Register(atype)
+}
+
+func RegisterEntryTypeVersions(s EntryTypeVersionScheme) {
+ DefaultEntryTypeScheme().AddKnownTypes(s)
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/suite_test.go b/api/ocm/extensions/labels/routingslip/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/labels/routingslip/suite_test.go
rename to api/ocm/extensions/labels/routingslip/suite_test.go
diff --git a/api/ocm/extensions/labels/routingslip/transfer_test.go b/api/ocm/extensions/labels/routingslip/transfer_test.go
new file mode 100644
index 000000000..113efc458
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/transfer_test.go
@@ -0,0 +1,108 @@
+package routingslip_test
+
+import (
+ "fmt"
+
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/mandelsoft/goutils/finalizer"
+
+ "ocm.software/ocm/api/helper/builder"
+ "ocm.software/ocm/api/ocm/extensions/attrs/compositionmodeattr"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types/comment"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/tools/transfer"
+ "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard"
+ "ocm.software/ocm/api/tech/signing/handlers/rsa"
+ "ocm.software/ocm/api/utils/accessobj"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ ARCH = "/tmp/ctf"
+ TARGET = "/tmp/target"
+ COMPONENT = "acme.org/routingslip"
+ VERSION = "1.0.0"
+ LOCAL = "local.org"
+)
+
+var _ = Describe("management", func() {
+ var env *builder.Builder
+
+ BeforeEach(func() {
+ env = builder.NewBuilder()
+ env.RSAKeyPair(ORG, LOCAL)
+ })
+
+ AfterEach(func() {
+ env.Cleanup()
+ })
+
+ DescribeTable("transfers and updates", func(mode bool) {
+ var finalize finalizer.Finalizer
+
+ defer Defer(finalize.Finalize, "finalizer")
+
+ compositionmodeattr.Set(env.OCMContext(), mode)
+ e1 := comment.New("start of routing slip")
+ e2 := comment.New("additional entry")
+
+ repo := Must(ctf.Open(env, accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, ARCH, 0o700, env))
+ finalize.Close(repo, "repo")
+
+ c := Must(repo.LookupComponent(COMPONENT))
+ finalize.Close(c, "comp")
+ cv := Must(c.NewVersion(VERSION))
+ finalize.Close(cv, "vers")
+ cv.GetDescriptor().Provider.Name = ORG
+ MustBeSuccessful(routingslip.AddEntry(cv, ORG, rsa.Algorithm, e1, nil))
+ MustBeSuccessful(c.AddVersion(cv))
+
+ target := Must(ctf.Open(env, accessobj.ACC_WRITABLE|accessobj.ACC_CREATE, TARGET, 0o700, env))
+ finalize.Close(target, "target")
+ pr, buf := common.NewBufferedPrinter()
+
+ MustBeSuccessful(transfer.TransferVersion(pr, nil, cv, target, Must(standard.New())))
+
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(`
+transferring version "acme.org/routingslip:1.0.0"...
+...adding component version...
+`))
+ nested := finalize.Nested()
+ tc := Must(target.LookupComponent(COMPONENT))
+ nested.Close(tc, "target comp")
+ tcv := Must(tc.LookupVersion(VERSION))
+ nested.Close(tcv)
+
+ slip := Must(routingslip.GetSlip(tcv, ORG))
+ MustBeSuccessful(routingslip.AddEntry(tcv, LOCAL, rsa.Algorithm, e1, nil))
+ Expect(slip.Len()).To(Equal(1))
+
+ MustBeSuccessful(tc.AddVersion(tcv))
+ MustBeSuccessful(nested.Finalize())
+
+ buf.Reset()
+ MustBeSuccessful(routingslip.AddEntry(cv, ORG, rsa.Algorithm, e2, nil))
+ MustBeSuccessful(transfer.TransferVersion(pr, nil, cv, target, Must(standard.New())))
+ Expect(buf.String()).To(StringEqualTrimmedWithContext(`
+transferring version "acme.org/routingslip:1.0.0"...
+ updating volatile properties of "acme.org/routingslip:1.0.0"
+...adding component version...
+`))
+
+ tcv = Must(target.LookupComponentVersion(COMPONENT, VERSION))
+ finalize.Close(tcv, "target")
+ label := Must(routingslip.Get(tcv))
+ Expect(len(label)).To(Equal(2))
+ Expect(len(label[ORG])).To(Equal(2))
+ Expect(len(label[LOCAL])).To(Equal(1))
+ fmt.Printf("*** routing slips:\n%s\n", Must(runtime.DefaultYAMLEncoding.Marshal(label)))
+ },
+ Entry("with direct mode", false),
+ Entry("with composition mode", true),
+ )
+})
diff --git a/api/ocm/extensions/labels/routingslip/types/comment/cli.go b/api/ocm/extensions/labels/routingslip/types/comment/cli.go
new file mode 100644
index 000000000..dd370e8cf
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/comment/cli.go
@@ -0,0 +1,30 @@
+package comment
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+func ConfigHandler() flagsets.ConfigOptionTypeSetHandler {
+ return flagsets.NewConfigOptionTypeSetHandler(
+ Type, AddConfig,
+ options.CommentOption,
+ )
+}
+
+func AddConfig(opts flagsets.ConfigOptions, config flagsets.Config) error {
+ flagsets.AddFieldByOptionP(opts, options.CommentOption, config, "comment")
+ return nil
+}
+
+var usage = `
+An unstructured comment as entry in a routing slip.
+`
+
+var formatV1 = `
+The type specific specification fields are:
+
+- **comment
** *string*
+
+ Any text as entry in a routing slip.
+`
diff --git a/api/ocm/extensions/labels/routingslip/types/comment/entry.go b/api/ocm/extensions/labels/routingslip/types/comment/entry.go
new file mode 100644
index 000000000..f59b7b27e
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/comment/entry.go
@@ -0,0 +1,45 @@
+package comment
+
+import (
+ "fmt"
+
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/spi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+// Type is the access type for a blob in an OCI repository.
+const (
+ Type = "comment"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ spi.Register(spi.NewEntryType[*Entry](Type, spi.WithDescription(usage)))
+ spi.Register(spi.NewEntryType[*Entry](TypeV1, spi.WithFormatSpec(formatV1), spi.WithConfigHandler(ConfigHandler())))
+}
+
+// New creates a new Helm Chart accessor for helm repositories.
+func New(comment string) *Entry {
+ return &Entry{
+ ObjectVersionedType: runtime.NewVersionedTypedObject(Type),
+ Comment: comment,
+ }
+}
+
+// Entry describes the access for a helm repository.
+type Entry struct {
+ runtime.ObjectVersionedType `json:",inline"`
+
+ // Comment is just a descriptive text in a routing slip-
+ Comment string `json:"comment"`
+}
+
+var _ spi.Entry = (*Entry)(nil)
+
+func (a *Entry) Describe(ctx spi.Context) string {
+ return fmt.Sprintf("Comment: %s", a.Comment)
+}
+
+func (a *Entry) Validate(spi.Context) error {
+ return nil
+}
diff --git a/api/ocm/extensions/labels/routingslip/types/init.go b/api/ocm/extensions/labels/routingslip/types/init.go
new file mode 100644
index 000000000..b48624464
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/init.go
@@ -0,0 +1,5 @@
+package types
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/types/comment"
+)
diff --git a/api/ocm/extensions/labels/routingslip/types/plugin/cmd_test.go b/api/ocm/extensions/labels/routingslip/types/plugin/cmd_test.go
new file mode 100644
index 000000000..0c691ca9b
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/plugin/cmd_test.go
@@ -0,0 +1,74 @@
+package plugin_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/env"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
+
+ "github.com/mandelsoft/goutils/sliceutils"
+ "github.com/mandelsoft/goutils/transformer"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+)
+
+const (
+ ARCH = "/tmp/ca"
+ VERSION = "v1"
+ COMP = "test.de/x"
+ PROVIDER = "acme.org"
+)
+
+var _ = Describe("Test Environment", func() {
+ var env *Environment
+ var plugins TempPluginDir
+
+ BeforeEach(func() {
+ env = NewEnvironment(TestData())
+
+ ctx := env.OCMContext()
+ plugins = Must(ConfigureTestPlugins(env, "testdata"))
+ registry := plugincacheattr.Get(ctx)
+ Expect(registration.RegisterExtensions(ctx)).To(Succeed())
+ p := registry.Get("test")
+ Expect(p).NotTo(BeNil())
+ })
+
+ AfterEach(func() {
+ plugins.Cleanup()
+ env.Cleanup()
+ })
+
+ It("handles plugin based entry type", func() {
+ prov := routingslip.For(env.OCMContext()).CreateConfigTypeSetConfigProvider()
+ configopts := prov.CreateOptions()
+ Expect(sliceutils.Transform(configopts.Options(), transformer.GetName[flagsets.Option, string])).To(ConsistOf(
+ "entry", "comment", // default settings
+ "mediaType", "accessPath", // by plugin
+ ))
+
+ fs := &pflag.FlagSet{}
+ fs.SortFlags = true
+ configopts.AddFlags(fs)
+ Expect("\n" + fs.FlagUsages()).To(Equal(`
+ --accessPath string file path
+ --comment string comment field value
+ --entry YAML routing slip entry specification (YAML)
+ --mediaType string media type for artifact blob representation
+`))
+ MustBeSuccessful(fs.Parse([]string{"--accessPath", "some path", "--" + options.MediatypeOption.GetName(), "media type"}))
+ prov.SetTypeName("test")
+ data := Must(prov.GetConfigFor(configopts))
+ Expect(data).To(YAMLEqual(`
+type: test
+mediaType: media type
+path: some path
+`))
+ })
+})
diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/doc.go b/api/ocm/extensions/labels/routingslip/types/plugin/doc.go
similarity index 100%
rename from pkg/contexts/ocm/labels/routingslip/types/plugin/doc.go
rename to api/ocm/extensions/labels/routingslip/types/plugin/doc.go
diff --git a/api/ocm/extensions/labels/routingslip/types/plugin/entry.go b/api/ocm/extensions/labels/routingslip/types/plugin/entry.go
new file mode 100644
index 000000000..dc10584da
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/plugin/entry.go
@@ -0,0 +1,27 @@
+package plugin
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/spi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type Entry struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+ handler *PluginHandler
+}
+
+var _ spi.Entry = &Entry{}
+
+func (s *Entry) Describe(ctx cpi.Context) string {
+ return s.handler.Describe(s, ctx)
+}
+
+func (s *Entry) Validate(ctx spi.Context) error {
+ _, err := s.handler.Validate(s)
+ return err
+}
+
+func (s *Entry) Handler() *PluginHandler {
+ return s.handler
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go b/api/ocm/extensions/labels/routingslip/types/plugin/entry_test.go
similarity index 75%
rename from pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go
rename to api/ocm/extensions/labels/routingslip/types/plugin/entry_test.go
index 9f98c9443..4a3b3f460 100644
--- a/pkg/contexts/ocm/labels/routingslip/types/plugin/entry_test.go
+++ b/api/ocm/extensions/labels/routingslip/types/plugin/entry_test.go
@@ -6,16 +6,16 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils"
- . "github.com/open-component-model/ocm/pkg/env"
+ . "ocm.software/ocm/api/helper/env"
+ . "ocm.software/ocm/api/ocm/plugin/testutils"
"github.com/spf13/pflag"
- "github.com/open-component-model/ocm/pkg/cobrautils/flagsets"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/labels/routingslip/spi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/registration"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/spi"
+ "ocm.software/ocm/api/ocm/plugin/plugins"
+ "ocm.software/ocm/api/ocm/plugin/registration"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
)
var _ = Describe("setup plugin cache", func() {
diff --git a/api/ocm/extensions/labels/routingslip/types/plugin/plugin.go b/api/ocm/extensions/labels/routingslip/types/plugin/plugin.go
new file mode 100644
index 000000000..991761206
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/plugin/plugin.go
@@ -0,0 +1,40 @@
+package plugin
+
+import (
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+)
+
+type plug = plugin.Plugin
+
+// PluginHandler is a shared object between the AccessMethod implementation and the Entry implementation. The
+// object knows the actual plugin and can therefore forward the method calls to corresponding cli commands.
+type PluginHandler struct {
+ plug
+}
+
+func NewPluginHandler(p plugin.Plugin) *PluginHandler {
+ return &PluginHandler{plug: p}
+}
+
+func (p *PluginHandler) Describe(spec *Entry, ctx cpi.Context) string {
+ sspec := p.GetValueSetDescriptor(descriptor.PURPOSE_ROUTINGSLIP, spec.GetKind(), spec.GetVersion())
+ if sspec == nil {
+ return "unknown type " + spec.GetType()
+ }
+ info, err := p.Validate(spec)
+ if err != nil {
+ return err.Error()
+ }
+ return info.Short
+}
+
+func (p *PluginHandler) Validate(spec *Entry) (*ppi.ValueSetInfo, error) {
+ data, err := spec.GetRaw()
+ if err != nil {
+ return nil, err
+ }
+ return p.plug.ValidateValueSet(descriptor.PURPOSE_ROUTINGSLIP, data)
+}
diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/suite_test.go b/api/ocm/extensions/labels/routingslip/types/plugin/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/labels/routingslip/types/plugin/suite_test.go
rename to api/ocm/extensions/labels/routingslip/types/plugin/suite_test.go
diff --git a/pkg/contexts/ocm/labels/routingslip/types/plugin/testdata/test b/api/ocm/extensions/labels/routingslip/types/plugin/testdata/test
similarity index 100%
rename from pkg/contexts/ocm/labels/routingslip/types/plugin/testdata/test
rename to api/ocm/extensions/labels/routingslip/types/plugin/testdata/test
diff --git a/api/ocm/extensions/labels/routingslip/types/plugin/type.go b/api/ocm/extensions/labels/routingslip/types/plugin/type.go
new file mode 100644
index 000000000..e096992b4
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/types/plugin/type.go
@@ -0,0 +1,70 @@
+package plugin
+
+import (
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/ocm/extensions/labels/routingslip/spi"
+ "ocm.software/ocm/api/ocm/plugin"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type entryType struct {
+ spi.EntryType
+ plug plugin.Plugin
+ cliopts flagsets.ConfigOptionTypeSet
+}
+
+var _ spi.EntryType = (*entryType)(nil)
+
+func NewType(name string, p plugin.Plugin, desc *plugin.ValueSetDescriptor) spi.EntryType {
+ format := desc.Format
+ if format != "" {
+ format = "\n" + format
+ }
+
+ t := &entryType{
+ plug: p,
+ }
+
+ cfghdlr := flagsets.NewConfigOptionTypeSetHandler(name, t.AddConfig)
+ for _, o := range desc.CLIOptions {
+ var opt flagsets.ConfigOptionType
+ if o.Type == "" {
+ opt = options.DefaultRegistry.GetOptionType(o.Name)
+ if opt == nil {
+ p.Context().Logger(plugin.TAG).Warn("unknown option", "plugin", p.Name(), "valueset", name, "option", o.Name)
+ }
+ } else {
+ var err error
+ opt, err = options.DefaultRegistry.CreateOptionType(o.Type, o.Name, o.Description)
+ if err != nil {
+ p.Context().Logger(plugin.TAG).Warn("invalid option", "plugin", p.Name(), "valueset", name, "option", o.Name, "error", err.Error())
+ }
+ }
+ if opt != nil {
+ cfghdlr.AddOptionType(opt)
+ }
+ }
+ aopts := []spi.EntryTypeOption{spi.WithDescription(desc.Description), spi.WithFormatSpec(format)}
+ if cfghdlr.Size() > 0 {
+ aopts = append(aopts, spi.WithConfigHandler(cfghdlr))
+ t.cliopts = cfghdlr
+ }
+ t.EntryType = spi.NewEntryType[*Entry](name, aopts...)
+ return t
+}
+
+func (t *entryType) Decode(data []byte, unmarshaler runtime.Unmarshaler) (spi.Entry, error) {
+ spec, err := t.EntryType.Decode(data, unmarshaler)
+ if err != nil {
+ return nil, err
+ }
+ spec.(*Entry).handler = NewPluginHandler(t.plug)
+ return spec, nil
+}
+
+func (t *entryType) AddConfig(opts flagsets.ConfigOptions, cfg flagsets.Config) error {
+ opts = opts.FilterBy(t.cliopts.HasOptionType)
+ return t.plug.ComposeValueSet(descriptor.PURPOSE_ROUTINGSLIP, t.GetType(), opts, cfg)
+}
diff --git a/api/ocm/extensions/labels/routingslip/usage.go b/api/ocm/extensions/labels/routingslip/usage.go
new file mode 100644
index 000000000..2d61401e3
--- /dev/null
+++ b/api/ocm/extensions/labels/routingslip/usage.go
@@ -0,0 +1,79 @@
+package routingslip
+
+import (
+ "fmt"
+ "strings"
+
+ "ocm.software/ocm/api/utils"
+ "ocm.software/ocm/api/utils/cobrautils/flagsets"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+func EntryUsage(scheme EntryTypeScheme, cli bool) string {
+ s := `
+The following list describes the well-known entry types explicitly supported
+by this version of the CLI, their versions and specification formats. Other
+kinds of entries can be configured using the --entry
option.
+`
+ type method struct {
+ desc string
+ versions map[string]string
+ options flagsets.ConfigOptionTypeSetHandler
+ }
+
+ descs := map[string]*method{}
+
+ // gather info for kinds and versions
+ for _, n := range scheme.KnownTypeNames() {
+ kind, vers := runtime.KindVersion(n)
+
+ info := descs[kind]
+ if info == nil {
+ info = &method{versions: map[string]string{}}
+ descs[kind] = info
+ }
+
+ if vers == "" {
+ vers = "v1"
+ }
+ if _, ok := info.versions[vers]; !ok {
+ info.versions[vers] = ""
+ }
+
+ t := scheme.GetType(n)
+
+ if t.ConfigOptionTypeSetHandler() != nil {
+ info.options = t.ConfigOptionTypeSetHandler()
+ }
+ desc := t.Description()
+ if desc != "" {
+ info.desc = desc
+ }
+
+ desc = t.Format()
+ if desc != "" {
+ info.versions[vers] = desc
+ }
+ }
+
+ for _, t := range utils.StringMapKeys(descs) {
+ info := descs[t]
+ desc := strings.Trim(info.desc, "\n")
+ if desc != "" {
+ s = fmt.Sprintf("%s\n- Entry type %s
\n\n%s\n\n", s, t, utils.IndentLines(desc, " "))
+
+ format := ""
+ for _, f := range utils.StringMapKeys(info.versions) {
+ desc = strings.Trim(info.versions[f], "\n")
+ if desc != "" {
+ format = fmt.Sprintf("%s\n- Version %s
\n\n%s\n", format, f, utils.IndentLines(desc, " "))
+ }
+ }
+ if format != "" {
+ s += fmt.Sprintf(" The following versions are supported:\n%s\n", strings.Trim(utils.IndentLines(format, " "), "\n"))
+ }
+ }
+ s += utils.IndentLines(flagsets.FormatConfigOptions(info.options), " ")
+ }
+ return s
+}
diff --git a/api/ocm/extensions/pubsub/attr.go b/api/ocm/extensions/pubsub/attr.go
new file mode 100644
index 000000000..fc8757ed5
--- /dev/null
+++ b/api/ocm/extensions/pubsub/attr.go
@@ -0,0 +1,40 @@
+package pubsub
+
+import (
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+const ATTR_PUBSUB_TYPES = "ocm.software/ocm/api/ocm/extensions/pubsub"
+
+type Attribute struct {
+ ProviderRegistry
+ TypeScheme
+}
+
+func For(ctx cpi.ContextProvider) *Attribute {
+ if ctx == nil {
+ return &Attribute{
+ ProviderRegistry: DefaultRegistry,
+ TypeScheme: DefaultTypeScheme,
+ }
+ }
+ return ctx.OCMContext().GetAttributes().GetOrCreateAttribute(ATTR_PUBSUB_TYPES, create).(*Attribute)
+}
+
+func create(datacontext.Context) interface{} {
+ return &Attribute{
+ ProviderRegistry: NewProviderRegistry(DefaultRegistry),
+ TypeScheme: NewTypeScheme(DefaultTypeScheme),
+ }
+}
+
+func SetSchemeFor(ctx cpi.ContextProvider, registry TypeScheme) {
+ attr := For(ctx)
+ attr.TypeScheme = registry
+}
+
+func SetProvidersFor(ctx cpi.ContextProvider, registry ProviderRegistry) {
+ attr := For(ctx)
+ attr.ProviderRegistry = registry
+}
diff --git a/pkg/contexts/ocm/pubsub/doc.go b/api/ocm/extensions/pubsub/doc.go
similarity index 100%
rename from pkg/contexts/ocm/pubsub/doc.go
rename to api/ocm/extensions/pubsub/doc.go
diff --git a/api/ocm/extensions/pubsub/interface.go b/api/ocm/extensions/pubsub/interface.go
new file mode 100644
index 000000000..744a14a82
--- /dev/null
+++ b/api/ocm/extensions/pubsub/interface.go
@@ -0,0 +1,225 @@
+package pubsub
+
+import (
+ "encoding/json"
+ "fmt"
+ "slices"
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "github.com/mandelsoft/goutils/generics"
+ "github.com/mandelsoft/goutils/optionutils"
+ "github.com/modern-go/reflect2"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/utils/errkind"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+ "ocm.software/ocm/api/utils/runtime/descriptivetype"
+)
+
+const KIND_PUBSUBTYPE = "pub/sub"
+
+type Option = descriptivetype.Option
+
+func WithFormatSpec(fmt string) Option {
+ return descriptivetype.WithFormatSpec(fmt)
+}
+
+func WithDesciption(desc string) Option {
+ return descriptivetype.WithDescription(desc)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type PubSubType descriptivetype.TypedObjectType[PubSubSpec]
+
+// PubSubSpec is the interface publish/subscribe specifications
+// must fulfill. The main task is to map the specification
+// to a concrete implementation of the pub/sub adapter
+// which forwards events to the described system.
+type PubSubSpec interface {
+ runtime.VersionedTypedObject
+
+ PubSubMethod(repo cpi.Repository) (PubSubMethod, error)
+ Describe(ctx cpi.Context) string
+}
+
+type (
+ PubSubSpecDecoder = runtime.TypedObjectDecoder[PubSubSpec]
+ PubSubTypeProvider = runtime.KnownTypesProvider[PubSubSpec, PubSubType]
+)
+
+// PubSubMethod is the handler able to publish
+// an OCM component version event.
+type PubSubMethod interface {
+ NotifyComponentVersion(version common.NameVersion) error
+}
+
+// TypeScheme is the registry for specification types for
+// PubSub types. A PubSub type is finally able to
+// provide an implementation for notifying a dedicated
+// PubSub instance.
+type TypeScheme descriptivetype.TypeScheme[PubSubSpec, PubSubType]
+
+func NewTypeScheme(base ...TypeScheme) TypeScheme {
+ return descriptivetype.NewTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...)
+}
+
+func NewStrictTypeScheme(base ...TypeScheme) runtime.VersionedTypeRegistry[PubSubSpec, PubSubType] {
+ return descriptivetype.NewTypeScheme[PubSubSpec, PubSubType, TypeScheme]("PubSub type", nil, &UnknownPubSubSpec{}, false, base...)
+}
+
+// DefaultTypeScheme contains all globally known PubSub serializers.
+var DefaultTypeScheme = NewTypeScheme()
+
+func RegisterType(atype PubSubType) {
+ DefaultTypeScheme.Register(atype)
+}
+
+func CreatePubSubSpec(t runtime.TypedObject) (PubSubSpec, error) {
+ return DefaultTypeScheme.Convert(t)
+}
+
+func NewPubSubType[I PubSubSpec](name string, opts ...Option) PubSubType {
+ t := descriptivetype.NewTypedObjectTypeObject[PubSubSpec](runtime.NewVersionedTypedObjectType[PubSubSpec, I](name))
+ ta := descriptivetype.NewTypeObjectTarget[PubSubSpec](t)
+ optionutils.ApplyOptions[descriptivetype.OptionTarget](ta, opts...)
+ return t
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type UnknownPubSubSpec struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+}
+
+var (
+ _ runtime.TypedObject = &UnknownPubSubSpec{}
+ _ runtime.Unknown = &UnknownPubSubSpec{}
+)
+
+func (_ *UnknownPubSubSpec) IsUnknown() bool {
+ return true
+}
+
+func (s *UnknownPubSubSpec) PubSubMethod(repository cpi.Repository) (PubSubMethod, error) {
+ return nil, errors.ErrUnknown(KIND_PUBSUBTYPE, s.GetType())
+}
+
+func (s *UnknownPubSubSpec) Describe(ctx cpi.Context) string {
+ return fmt.Sprintf("unknown PubSub specification type %q", s.GetType())
+}
+
+var _ PubSubSpec = &UnknownPubSubSpec{}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type Unwrapable interface {
+ Unwrap(ctx cpi.Context) []PubSubSpec
+}
+
+type Evaluatable interface {
+ Evaluate(ctx cpi.Context) (PubSubSpec, error)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type GenericPubSubSpec struct {
+ runtime.UnstructuredVersionedTypedObject `json:",inline"`
+
+ lock sync.Mutex
+ cached PubSubSpec
+ cachedData []byte
+}
+
+var (
+ _ PubSubSpec = &GenericPubSubSpec{}
+ _ Unwrapable = &GenericPubSubSpec{}
+ _ Evaluatable = &GenericPubSubSpec{}
+)
+
+func ToGenericPubSubSpec(spec PubSubSpec) (*GenericPubSubSpec, error) {
+ if reflect2.IsNil(spec) {
+ return nil, nil
+ }
+ if g, ok := spec.(*GenericPubSubSpec); ok {
+ return g, nil
+ }
+ data, err := json.Marshal(spec)
+ if err != nil {
+ return nil, err
+ }
+ return newGenericPubSubSpec(data, runtime.DefaultJSONEncoding)
+}
+
+func NewGenericPubSubSpec(data []byte, unmarshaler ...runtime.Unmarshaler) (PubSubSpec, error) {
+ return generics.CastPointerR[PubSubSpec](newGenericPubSubSpec(data, general.Optional(unmarshaler...)))
+}
+
+func newGenericPubSubSpec(data []byte, unmarshaler runtime.Unmarshaler) (*GenericPubSubSpec, error) {
+ unstr := &runtime.UnstructuredVersionedTypedObject{}
+ if unmarshaler == nil {
+ unmarshaler = runtime.DefaultYAMLEncoding
+ }
+ err := unmarshaler.Unmarshal(data, unstr)
+ if err != nil {
+ return nil, err
+ }
+ return &GenericPubSubSpec{UnstructuredVersionedTypedObject: *unstr}, nil
+}
+
+func (s *GenericPubSubSpec) Unwrap(ctx cpi.Context) []PubSubSpec {
+ eff, err := s.Evaluate(ctx)
+ if err != nil {
+ return nil
+ }
+ if u, ok := eff.(Unwrapable); ok {
+ return u.Unwrap(ctx)
+ }
+ return nil
+}
+
+func (s *GenericPubSubSpec) Describe(ctx cpi.Context) string {
+ eff, err := s.Evaluate(ctx)
+ if err != nil {
+ return fmt.Sprintf("invalid access specification: %s", err.Error())
+ }
+ return eff.Describe(ctx)
+}
+
+func (s *GenericPubSubSpec) Evaluate(ctx cpi.Context) (PubSubSpec, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if s.cached != nil && s.cachedData != nil {
+ if d, err := s.GetRaw(); err == nil {
+ if slices.Equal(d, s.cachedData) {
+ return s.cached, nil
+ }
+ }
+ s.cached = nil
+ s.cachedData = nil
+ }
+ raw, err := s.GetRaw()
+ if err != nil {
+ return nil, err
+ }
+ s.cached, err = For(ctx).TypeScheme.Decode(raw, runtime.DefaultJSONEncoding)
+ if err == nil {
+ s.cachedData = raw
+ }
+ return s.cached, err
+}
+
+func (s *GenericPubSubSpec) PubSubMethod(repository cpi.Repository) (PubSubMethod, error) {
+ spec, err := s.Evaluate(repository.GetContext())
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := spec.(*GenericPubSubSpec); ok {
+ return nil, errors.ErrUnknown(errkind.KIND_ACCESSMETHOD, s.GetType())
+ }
+ return spec.PubSubMethod(repository)
+}
diff --git a/api/ocm/extensions/pubsub/provider.go b/api/ocm/extensions/pubsub/provider.go
new file mode 100644
index 000000000..81251ddbd
--- /dev/null
+++ b/api/ocm/extensions/pubsub/provider.go
@@ -0,0 +1,99 @@
+package pubsub
+
+import (
+ "sync"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/general"
+ "golang.org/x/exp/maps"
+
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+// ProviderRegistry holds handlers able to extract
+// a PubSub specification for an OCM repository of a dedicated kind.
+type ProviderRegistry interface {
+ Register(repoKind string, prov Provider)
+ KnownProviders() map[string]Provider
+ AddKnownProviders(registry ProviderRegistry)
+
+ For(repo string) Provider
+}
+
+var DefaultRegistry = NewProviderRegistry()
+
+func RegisterProvider(repokind string, prov Provider) {
+ DefaultRegistry.Register(repokind, prov)
+}
+
+// A Provider is able to extract a pub sub configuration for
+// an ocm repository (typically registered for a dedicated type of repository).
+// It does not handle the pub sub system, but just the persistence of
+// a pub sub specification configured for a dedicated type of repository.
+type Provider interface {
+ GetPubSubSpec(repo cpi.Repository) (PubSubSpec, error)
+ SetPubSubSpec(repo cpi.Repository, spec PubSubSpec) error
+}
+
+type NopProvider struct{}
+
+func (p NopProvider) GetPubSubSpec(repo cpi.Repository) (PubSubSpec, error) {
+ return nil, nil
+}
+
+func (p NopProvider) SetPubSubSpec(repo cpi.Repository, spec PubSubSpec) error {
+ return errors.ErrNotSupported("pub/sub configuration")
+}
+
+func NewProviderRegistry(base ...ProviderRegistry) ProviderRegistry {
+ return &providers{
+ base: general.Optional(base...),
+ providers: map[string]Provider{},
+ }
+}
+
+type providers struct {
+ lock sync.Mutex
+
+ base ProviderRegistry
+ providers map[string]Provider
+}
+
+func (p *providers) Register(repoKind string, prov Provider) {
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ p.providers[repoKind] = prov
+}
+
+func (p *providers) For(repo string) Provider {
+ p.lock.Lock()
+ defer p.lock.Unlock()
+ prov := p.providers[repo]
+ if prov != nil {
+ return prov
+ }
+ if p.base != nil {
+ return p.base.For(repo)
+ }
+ return nil
+}
+
+func (p *providers) KnownProviders() map[string]Provider {
+ if p.base != nil {
+ m := p.base.KnownProviders()
+ for n, e := range p.providers {
+ if m[n] == nil {
+ m[n] = e
+ }
+ }
+ return m
+ }
+ return maps.Clone(p.providers)
+}
+
+func (p *providers) AddKnownProviders(base ProviderRegistry) {
+ for n, e := range base.KnownProviders() {
+ p.providers[n] = e
+ }
+}
diff --git a/api/ocm/extensions/pubsub/providers/init.go b/api/ocm/extensions/pubsub/providers/init.go
new file mode 100644
index 000000000..1f7eced2f
--- /dev/null
+++ b/api/ocm/extensions/pubsub/providers/init.go
@@ -0,0 +1,5 @@
+package providers
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/pubsub/providers/ocireg"
+)
diff --git a/api/ocm/extensions/pubsub/providers/ocireg/provider.go b/api/ocm/extensions/pubsub/providers/ocireg/provider.go
new file mode 100644
index 000000000..4a391dec6
--- /dev/null
+++ b/api/ocm/extensions/pubsub/providers/ocireg/provider.go
@@ -0,0 +1,180 @@
+package ocireg
+
+import (
+ "encoding/json"
+ "fmt"
+ "path"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/cpi/repocpi"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ compound2 "ocm.software/ocm/api/ocm/extensions/pubsub/types/compound"
+ "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg"
+ "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg/componentmapping"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ocireg"
+ "ocm.software/ocm/api/utils/blobaccess/blobaccess"
+)
+
+const (
+ ConfigMimeType = "application/vnd.ocm.software.repository.config.v1+json"
+ PubSubLayerMimeTye = "application/vnd.ocm.software.repository.config.pubsub.v1+json"
+)
+
+const META = "meta"
+
+func init() {
+ pubsub.RegisterProvider(ocireg.Type, &Provider{})
+}
+
+type Provider struct{}
+
+var _ pubsub.Provider = (*Provider)(nil)
+
+func (p *Provider) GetPubSubSpec(repo repocpi.Repository) (pubsub.PubSubSpec, error) {
+ impl, err := repocpi.GetRepositoryImplementation(repo)
+ if err != nil {
+ return nil, err
+ }
+ gen, ok := impl.(*genericocireg.RepositoryImpl)
+ if !ok {
+ return nil, errors.ErrNotSupported("non-oci based ocm repository")
+ }
+
+ ocirepo := path.Join(gen.Meta().SubPath, componentmapping.ComponentDescriptorNamespace)
+ acc, err := gen.OCIRepository().LookupArtifact(ocirepo, META)
+ if errors.IsErrNotFound(err) || errors.IsErrUnknown(err) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot access meta data manifest version")
+ }
+ defer acc.Close()
+ m := acc.ManifestAccess()
+ if m == nil {
+ return nil, fmt.Errorf("meta data artifact is no manifest artifact")
+ }
+ if m.GetDescriptor().Config.MediaType != ConfigMimeType {
+ return nil, fmt.Errorf("meta data artifact has unexpected mime type %q", m.GetDescriptor().Config.MediaType)
+ }
+ compound, _ := compound2.New()
+ for _, l := range m.GetDescriptor().Layers {
+ if l.MediaType == PubSubLayerMimeTye {
+ var ps pubsub.GenericPubSubSpec
+
+ blob, err := m.GetBlob(l.Digest)
+ if err != nil {
+ return nil, err
+ }
+ data, err := blob.Get()
+ blob.Close()
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(data, &ps)
+ if err != nil {
+ return nil, err
+ }
+ compound.Specifications = append(compound.Specifications, &ps)
+ }
+ }
+ return compound.Effective(), nil
+}
+
+func (p *Provider) SetPubSubSpec(repo cpi.Repository, spec pubsub.PubSubSpec) error {
+ impl, err := repocpi.GetRepositoryImplementation(repo)
+ if err != nil {
+ return err
+ }
+ gen, ok := impl.(*genericocireg.RepositoryImpl)
+ if !ok {
+ return errors.ErrNotSupported("non-oci based ocm repository")
+ }
+
+ var data []byte
+ if spec != nil {
+ data, err = json.Marshal(spec)
+ if err != nil {
+ return err
+ }
+ }
+
+ ocirepo := path.Join(gen.Meta().SubPath, componentmapping.ComponentDescriptorNamespace)
+ ns, err := gen.OCIRepository().LookupNamespace(ocirepo)
+ if err != nil {
+ return err
+ }
+ defer ns.Close()
+
+ acc, err := ns.GetArtifact(META)
+ if err != nil {
+ if errors.IsErrNotFound(err) || errors.IsErrUnknown(err) {
+ if spec == nil {
+ return nil
+ }
+ } else {
+ return err
+ }
+ }
+ if acc == nil {
+ acc, err = ns.NewArtifact()
+ if err != nil {
+ return err
+ }
+ m, err := acc.Manifest()
+ if err != nil {
+ return err
+ }
+ config := blobaccess.ForString(ConfigMimeType, "{}")
+ m.Config.MediaType = config.MimeType()
+ m.Config.Digest = config.Digest()
+ err = acc.AddBlob(config)
+ if err != nil {
+ return err
+ }
+ }
+ defer acc.Close()
+
+ m := acc.ManifestAccess()
+ if m == nil {
+ return fmt.Errorf("meta data artifact is no manifest artifact")
+ }
+ if m.GetDescriptor().Config.MediaType != ConfigMimeType {
+ return fmt.Errorf("meta data artifact has unexpected mime type %q", m.GetDescriptor().Config.MediaType)
+ }
+
+ blob := blobaccess.ForData(PubSubLayerMimeTye, data)
+ defer blob.Close()
+
+ layers := m.GetDescriptor().Layers
+ for i := 0; i < len(layers); i++ {
+ l := layers[i]
+ if l.MediaType == PubSubLayerMimeTye {
+ if data != nil {
+ m.AddBlob(blob)
+ l.Digest = blob.Digest()
+ b, err := ns.AddArtifact(m, META)
+ if b != nil {
+ b.Close()
+ }
+ return err
+ } else {
+ layers = append(layers[:i], layers[i+1:]...)
+ i--
+ }
+ }
+ }
+ m.GetDescriptor().Layers = layers
+ if data != nil {
+ _, err = m.AddLayer(blob, nil)
+ if err != nil {
+ return err
+ }
+ }
+ b, err := ns.AddArtifact(m, META)
+ if b != nil {
+ b.Close()
+ }
+ return err
+}
diff --git a/pkg/contexts/ocm/pubsub/providers/ocireg/provider_test.go b/api/ocm/extensions/pubsub/providers/ocireg/provider_test.go
similarity index 77%
rename from pkg/contexts/ocm/pubsub/providers/ocireg/provider_test.go
rename to api/ocm/extensions/pubsub/providers/ocireg/provider_test.go
index 0103755a1..986551e58 100644
--- a/pkg/contexts/ocm/pubsub/providers/ocireg/provider_test.go
+++ b/api/ocm/extensions/pubsub/providers/ocireg/provider_test.go
@@ -6,16 +6,16 @@ import (
. "github.com/mandelsoft/goutils/testutils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- . "github.com/open-component-model/ocm/pkg/env/builder"
-
- "github.com/open-component-model/ocm/pkg/common/accessio"
- ocictf "github.com/open-component-model/ocm/pkg/contexts/oci/repositories/ctf"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/pubsub"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/pubsub/providers/ocireg"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/ctf"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/genericocireg/componentmapping"
- "github.com/open-component-model/ocm/pkg/runtime"
+ . "ocm.software/ocm/api/helper/builder"
+
+ ocictf "ocm.software/ocm/api/oci/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/providers/ocireg"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg/componentmapping"
+ "ocm.software/ocm/api/utils/accessio"
+ "ocm.software/ocm/api/utils/runtime"
)
const ARCH = "ctf"
diff --git a/pkg/contexts/ocm/pubsub/providers/ocireg/suite_test.go b/api/ocm/extensions/pubsub/providers/ocireg/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/pubsub/providers/ocireg/suite_test.go
rename to api/ocm/extensions/pubsub/providers/ocireg/suite_test.go
diff --git a/pkg/contexts/ocm/pubsub/pubsub_test.go b/api/ocm/extensions/pubsub/pubsub_test.go
similarity index 87%
rename from pkg/contexts/ocm/pubsub/pubsub_test.go
rename to api/ocm/extensions/pubsub/pubsub_test.go
index c824eb532..cac6033e3 100644
--- a/pkg/contexts/ocm/pubsub/pubsub_test.go
+++ b/api/ocm/extensions/pubsub/pubsub_test.go
@@ -11,14 +11,14 @@ import (
"github.com/mandelsoft/goutils/sliceutils"
- "github.com/open-component-model/ocm/pkg/common"
- "github.com/open-component-model/ocm/pkg/contexts/datacontext"
- "github.com/open-component-model/ocm/pkg/contexts/ocm"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/pubsub"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/pubsub/types/compound"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/repositories/composition"
- "github.com/open-component-model/ocm/pkg/runtime"
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/types/compound"
+ "ocm.software/ocm/api/ocm/extensions/repositories/composition"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
)
const (
diff --git a/api/ocm/extensions/pubsub/setup.go b/api/ocm/extensions/pubsub/setup.go
new file mode 100644
index 000000000..51479a769
--- /dev/null
+++ b/api/ocm/extensions/pubsub/setup.go
@@ -0,0 +1,34 @@
+package pubsub
+
+import (
+ "ocm.software/ocm/api/datacontext"
+ "ocm.software/ocm/api/ocm/cpi"
+)
+
+func init() {
+ datacontext.RegisterSetupHandler(datacontext.SetupHandlerFunction(setupContext))
+}
+
+func setupContext(mode datacontext.BuilderMode, ctx datacontext.Context) {
+ if octx, ok := ctx.(cpi.Context); ok {
+ switch mode {
+ case datacontext.MODE_SHARED:
+ fallthrough
+ case datacontext.MODE_DEFAULTED:
+ // do nothing, fallback to the default attribute lookup
+ case datacontext.MODE_EXTENDED:
+ SetSchemeFor(octx, NewTypeScheme(DefaultTypeScheme))
+ SetProvidersFor(octx, NewProviderRegistry(DefaultRegistry))
+ case datacontext.MODE_CONFIGURED:
+ s := NewTypeScheme(nil)
+ s.AddKnownTypes(DefaultTypeScheme)
+ SetSchemeFor(octx, s)
+ r := NewProviderRegistry(nil)
+ r.AddKnownProviders(DefaultRegistry)
+ SetProvidersFor(octx, r)
+ case datacontext.MODE_INITIAL:
+ SetSchemeFor(octx, NewTypeScheme())
+ SetProvidersFor(octx, NewProviderRegistry())
+ }
+ }
+}
diff --git a/pkg/contexts/ocm/pubsub/suite_test.go b/api/ocm/extensions/pubsub/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/pubsub/suite_test.go
rename to api/ocm/extensions/pubsub/suite_test.go
diff --git a/api/ocm/extensions/pubsub/types/compound/type.go b/api/ocm/extensions/pubsub/types/compound/type.go
new file mode 100644
index 000000000..7505344d9
--- /dev/null
+++ b/api/ocm/extensions/pubsub/types/compound/type.go
@@ -0,0 +1,102 @@
+package compound
+
+import (
+ "fmt"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/mandelsoft/goutils/sliceutils"
+
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "compound"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ pubsub.RegisterType(pubsub.NewPubSubType[*Spec](Type,
+ pubsub.WithDesciption("A pub/sub system forwarding events to described sub-level systems.")))
+ pubsub.RegisterType(pubsub.NewPubSubType[*Spec](TypeV1,
+ pubsub.WithFormatSpec(`It is described by the following field:
+
+- **specifications
** *list of pubsub specs*
+
+ A list of nested sub-level specifications the events should be
+ forwarded to.
+`)))
+}
+
+// Spec provides a pub sub adapter registering events at its provider.
+type Spec struct {
+ runtime.ObjectVersionedType
+ Specifications []*pubsub.GenericPubSubSpec `json:"specifications,omitempty"`
+}
+
+var (
+ _ pubsub.PubSubSpec = (*Spec)(nil)
+ _ pubsub.Unwrapable = (*Spec)(nil)
+)
+
+func New(specs ...pubsub.PubSubSpec) (*Spec, error) {
+ var gen []*pubsub.GenericPubSubSpec
+
+ for _, s := range specs {
+ g, err := pubsub.ToGenericPubSubSpec(s)
+ if err != nil {
+ return nil, err
+ }
+ gen = append(gen, g)
+ }
+ return &Spec{runtime.NewVersionedObjectType(Type), gen}, nil
+}
+
+func (s *Spec) PubSubMethod(repo cpi.Repository) (pubsub.PubSubMethod, error) {
+ var meths []pubsub.PubSubMethod
+
+ for _, e := range s.Specifications {
+ m, err := e.PubSubMethod(repo)
+ if err != nil {
+ return nil, err
+ }
+ meths = append(meths, m)
+ }
+ return &Method{meths}, nil
+}
+
+func (s *Spec) Unwrap(ctx cpi.Context) []pubsub.PubSubSpec {
+ return sliceutils.Convert[pubsub.PubSubSpec](s.Specifications)
+}
+
+func (s *Spec) Describe(_ cpi.Context) string {
+ return fmt.Sprintf("compound pub/sub specification with %d entries", len(s.Specifications))
+}
+
+func (s *Spec) Effective() pubsub.PubSubSpec {
+ switch len(s.Specifications) {
+ case 0:
+ return nil
+ case 1:
+ return s.Specifications[0]
+ default:
+ return s
+ }
+}
+
+// Method finally registers events at contained methods.
+type Method struct {
+ meths []pubsub.PubSubMethod
+}
+
+var _ pubsub.PubSubMethod = (*Method)(nil)
+
+func (m *Method) NotifyComponentVersion(version common.NameVersion) error {
+ list := errors.ErrList()
+ for _, m := range m.meths {
+ list.Add(m.NotifyComponentVersion(version))
+ }
+ return list.Result()
+}
diff --git a/api/ocm/extensions/pubsub/types/init.go b/api/ocm/extensions/pubsub/types/init.go
new file mode 100644
index 000000000..2e14ba8e5
--- /dev/null
+++ b/api/ocm/extensions/pubsub/types/init.go
@@ -0,0 +1,6 @@
+package types
+
+import (
+ _ "ocm.software/ocm/api/ocm/extensions/pubsub/types/compound"
+ _ "ocm.software/ocm/api/ocm/extensions/pubsub/types/redis"
+)
diff --git a/api/ocm/extensions/pubsub/types/redis/identity/identity.go b/api/ocm/extensions/pubsub/types/redis/identity/identity.go
new file mode 100644
index 000000000..f0cdd18d4
--- /dev/null
+++ b/api/ocm/extensions/pubsub/types/redis/identity/identity.go
@@ -0,0 +1,120 @@
+package identity
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/mandelsoft/goutils/errors"
+
+ "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/credentials/identity/hostpath"
+ "ocm.software/ocm/api/utils/listformat"
+)
+
+const CONSUMER_TYPE = "Github"
+
+// identity properties.
+const (
+ ID_HOSTNAME = hostpath.ID_HOSTNAME
+ ID_PORT = hostpath.ID_PORT
+ ID_PATHPREFIX = hostpath.ID_PATHPREFIX
+ ID_CHANNEL = "channel"
+ ID_DATABASE = "database"
+)
+
+// credential properties.
+const (
+ ATTR_USERNAME = cpi.ATTR_USERNAME
+ ATTR_PASSWORD = cpi.ATTR_PASSWORD
+)
+
+func IdentityMatcher(request, cur, id cpi.ConsumerIdentity) bool {
+ match, better := hostpath.Match(CONSUMER_TYPE, request, cur, id)
+ if !match {
+ return false
+ }
+
+ if request[ID_CHANNEL] != "" {
+ if id[ID_CHANNEL] != "" && id[ID_CHANNEL] != request[ID_CHANNEL] {
+ return false
+ }
+ }
+ if request[ID_DATABASE] != "" {
+ if id[ID_DATABASE] != "" && id[ID_DATABASE] != request[ID_DATABASE] {
+ return false
+ }
+ }
+
+ // ok now it basically matches, check against current match
+
+ if cur[ID_CHANNEL] == "" && request[ID_CHANNEL] != "" {
+ return true
+ }
+ if cur[ID_DATABASE] == "" && request[ID_DATABASE] != "" {
+ return true
+ }
+ return better
+}
+
+func init() {
+ attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
+ ATTR_USERNAME, "Redis username",
+ ATTR_PASSWORD, "Redis password",
+ })
+ cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher,
+ `Redis PubSub credential matcher
+
+This matcher is a hostpath matcher with additional attributes:
+
+- *`+ID_CHANNEL+`
* (required if set in pattern): the channel name
+- *`+ID_DATABASE+`
* the database number
+`,
+ attrs)
+}
+
+func PATCredentials(user, pass string) cpi.Credentials {
+ return cpi.DirectCredentials{
+ ATTR_USERNAME: user,
+ ATTR_PASSWORD: pass,
+ }
+}
+
+func GetConsumerId(serveraddr string, channel string, db int) cpi.ConsumerIdentity {
+ p := ""
+ host, port, err := ParseAddress(serveraddr)
+ if err == nil {
+ host = serveraddr
+ }
+
+ id := cpi.ConsumerIdentity{
+ cpi.ID_TYPE: CONSUMER_TYPE,
+ ID_HOSTNAME: host,
+ ID_CHANNEL: channel,
+ ID_DATABASE: fmt.Sprintf("%d", db),
+ }
+ if port != 0 {
+ id[ID_PORT] = fmt.Sprintf("%d", port)
+ }
+ if p != "" {
+ id[ID_PATHPREFIX] = p
+ }
+ return id
+}
+
+func GetCredentials(ctx cpi.ContextProvider, serverurl string, channel string, db int) (cpi.Credentials, error) {
+ id := GetConsumerId(serverurl, channel, db)
+ return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id, IdentityMatcher)
+}
+
+func ParseAddress(addr string) (string, int, error) {
+ idx := strings.Index(addr, ":")
+ if idx < 0 {
+ return addr, 6379, nil
+ }
+ p, err := strconv.ParseInt(addr[idx+1:], 10, 32)
+ if err != nil {
+ return "", 0, errors.Wrapf(err, "invalid port in redis address")
+ }
+ return addr[:idx], int(p), nil
+}
diff --git a/api/ocm/extensions/pubsub/types/redis/redis_test.go b/api/ocm/extensions/pubsub/types/redis/redis_test.go
new file mode 100644
index 000000000..8c1176fac
--- /dev/null
+++ b/api/ocm/extensions/pubsub/types/redis/redis_test.go
@@ -0,0 +1,65 @@
+//go:build redis_test
+
+package redis_test
+
+import (
+ . "github.com/mandelsoft/goutils/testutils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ . "ocm.software/ocm/api/helper/builder"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/providers/ocireg"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/types/redis"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/types/redis/identity"
+ "ocm.software/ocm/api/ocm/extensions/repositories/composition"
+ "ocm.software/ocm/api/ocm/extensions/repositories/ctf"
+ "ocm.software/ocm/api/utils/accessio"
+ common "ocm.software/ocm/api/utils/misc"
+)
+
+const (
+ ARCH = "ctf"
+ COMP = "acme.org/component"
+ VERS = "v1"
+)
+
+var _ = Describe("Test Environment", func() {
+ var env *Builder
+ var repo ocm.Repository
+
+ BeforeEach(func() {
+ env = NewBuilder()
+ env.OCMCommonTransport(ARCH, accessio.FormatDirectory)
+ attr := pubsub.For(env)
+ attr.ProviderRegistry.Register(ctf.Type, &ocireg.Provider{})
+
+ env.CredentialsContext().SetCredentialsForConsumer(
+ identity.GetConsumerId("localhost:6379", "ocm", 0),
+ credentials.NewCredentials(common.Properties{identity.ATTR_PASSWORD: "redis-test-0815"}),
+ )
+
+ repo = Must(ctf.Open(env, ctf.ACC_WRITABLE, ARCH, 0o600, env))
+ })
+
+ AfterEach(func() {
+ if repo != nil {
+ MustBeSuccessful(repo.Close())
+ }
+ env.Cleanup()
+ })
+
+ Context("local redis server", func() {
+ It("tests local server", func() {
+ MustBeSuccessful(pubsub.SetForRepo(repo, Must(redis.New("localhost:6379", "ocm", 0))))
+
+ cv := composition.NewComponentVersion(env, COMP, VERS)
+ defer Close(cv)
+
+ Expect(repo.GetSpecification().GetKind()).To(Equal(ctf.Type))
+ MustBeSuccessful(repo.AddComponentVersion(cv))
+ })
+ })
+})
diff --git a/pkg/contexts/ocm/pubsub/types/redis/suite_test.go b/api/ocm/extensions/pubsub/types/redis/suite_test.go
similarity index 100%
rename from pkg/contexts/ocm/pubsub/types/redis/suite_test.go
rename to api/ocm/extensions/pubsub/types/redis/suite_test.go
diff --git a/api/ocm/extensions/pubsub/types/redis/type.go b/api/ocm/extensions/pubsub/types/redis/type.go
new file mode 100644
index 000000000..35113a45c
--- /dev/null
+++ b/api/ocm/extensions/pubsub/types/redis/type.go
@@ -0,0 +1,95 @@
+package redis
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/redis/go-redis/v9"
+
+ credcpi "ocm.software/ocm/api/credentials/cpi"
+ "ocm.software/ocm/api/ocm/cpi"
+ "ocm.software/ocm/api/ocm/extensions/pubsub"
+ "ocm.software/ocm/api/ocm/extensions/pubsub/types/redis/identity"
+ common "ocm.software/ocm/api/utils/misc"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Type = "redis"
+ TypeV1 = Type + runtime.VersionSeparator + "v1"
+)
+
+func init() {
+ pubsub.RegisterType(pubsub.NewPubSubType[*Spec](Type,
+ pubsub.WithDesciption("a redis pubsub sytsem.")))
+ pubsub.RegisterType(pubsub.NewPubSubType[*Spec](TypeV1,
+ pubsub.WithFormatSpec(`It is describe by the following field:
+
+- **serverAddr
** *Address of redis server*
+- **channel
** *pubsub channel*
+- **database
** *database number*
+
+ Publishing using the redis pubsub API. For every change a string message
+ with the format ` + ConfigType + `
can be used to configure a
+plugin.
+
++ type: ` + ConfigType + ` + plugin: <plugin name> + config: <arbitrary configuration structure> + disableAutoRegistration: <boolean flag to disable auto registration for up- and download handlers> ++` diff --git a/api/ocm/plugin/descriptor/const.go b/api/ocm/plugin/descriptor/const.go new file mode 100644 index 000000000..d2becd18a --- /dev/null +++ b/api/ocm/plugin/descriptor/const.go @@ -0,0 +1,19 @@ +package descriptor + +import ( + "ocm.software/ocm/api/datacontext/action" + "ocm.software/ocm/api/utils/errkind" + ocmlog "ocm.software/ocm/api/utils/logging" +) + +const ( + KIND_PLUGIN = "plugin" + KIND_DOWNLOADER = "downloader" + KIND_UPLOADER = "uploader" + KIND_ACCESSMETHOD = errkind.KIND_ACCESSMETHOD + KIND_ACTION = action.KIND_ACTION + KIND_VALUESET = "value set" + KIND_PURPOSE = "purposet" +) + +var REALM = ocmlog.DefineSubRealm("OCM plugin handling", "plugins") diff --git a/pkg/contexts/ocm/plugin/descriptor/descriptor.go b/api/ocm/plugin/descriptor/descriptor.go similarity index 98% rename from pkg/contexts/ocm/plugin/descriptor/descriptor.go rename to api/ocm/plugin/descriptor/descriptor.go index ec339a48b..3a371040c 100644 --- a/pkg/contexts/ocm/plugin/descriptor/descriptor.go +++ b/api/ocm/plugin/descriptor/descriptor.go @@ -3,7 +3,7 @@ package descriptor import ( "encoding/json" - metav1 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/meta/v1" + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" ) const VERSION = "v1" diff --git a/pkg/contexts/ocm/plugin/descriptor/doc.go b/api/ocm/plugin/descriptor/doc.go similarity index 100% rename from pkg/contexts/ocm/plugin/descriptor/doc.go rename to api/ocm/plugin/descriptor/doc.go diff --git a/pkg/contexts/ocm/plugin/descriptor/keys.go b/api/ocm/plugin/descriptor/keys.go similarity index 100% rename from pkg/contexts/ocm/plugin/descriptor/keys.go rename to api/ocm/plugin/descriptor/keys.go diff --git a/pkg/contexts/ocm/plugin/descriptor/suite_test.go b/api/ocm/plugin/descriptor/suite_test.go similarity index 100% rename from pkg/contexts/ocm/plugin/descriptor/suite_test.go rename to api/ocm/plugin/descriptor/suite_test.go diff --git a/api/ocm/plugin/descriptor/utils.go b/api/ocm/plugin/descriptor/utils.go new file mode 100644 index 000000000..e51e9dc06 --- /dev/null +++ b/api/ocm/plugin/descriptor/utils.go @@ -0,0 +1,58 @@ +package descriptor + +import ( + "sort" + + "github.com/mandelsoft/goutils/sliceutils" + + "ocm.software/ocm/api/ocm/ocmutils/registry" +) + +type Named interface { + GetName() string +} + +type StringName string + +func (e StringName) GetName() string { + return string(e) +} + +type Element[K registry.Key[K]] interface { + Named + GetConstraints() []K +} + +type List[T Named] []T + +func (l List[T]) Get(name string) *T { + for _, m := range l { + if m.GetName() == name { + return &m + } + } + return nil +} + +func (l List[T]) GetNames() []string { + var n []string + for _, e := range l { + n = append(n, e.GetName()) + } + sort.Strings(n) + return n +} + +func (l List[T]) MergeWith(o List[T]) List[T] { + var list []T +next: + for _, e := range o { + for _, f := range l { + if e.GetName() == f.GetName() { + continue next + } + } + list = append(list, e) + } + return sliceutils.CopyAppend(l, list...) +} diff --git a/pkg/contexts/ocm/plugin/descriptor/utils_test.go b/api/ocm/plugin/descriptor/utils_test.go similarity index 100% rename from pkg/contexts/ocm/plugin/descriptor/utils_test.go rename to api/ocm/plugin/descriptor/utils_test.go diff --git a/pkg/contexts/ocm/plugin/doc.go b/api/ocm/plugin/doc.go similarity index 100% rename from pkg/contexts/ocm/plugin/doc.go rename to api/ocm/plugin/doc.go diff --git a/api/ocm/plugin/interface.go b/api/ocm/plugin/interface.go new file mode 100644 index 000000000..4d4091179 --- /dev/null +++ b/api/ocm/plugin/interface.go @@ -0,0 +1,33 @@ +package plugin + +import ( + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/ocm/plugin/internal" +) + +const ( + KIND_PLUGIN = descriptor.KIND_PLUGIN + KIND_UPLOADER = descriptor.KIND_UPLOADER + KIND_ACCESSMETHOD = descriptor.KIND_ACCESSMETHOD + KIND_ACTION = descriptor.KIND_ACTION +) + +var TAG = descriptor.REALM + +type ( + Descriptor = descriptor.Descriptor + ActionDescriptor = descriptor.ActionDescriptor + ValueMergeHandlerDescriptor = descriptor.ValueMergeHandlerDescriptor + AccessMethodDescriptor = descriptor.AccessMethodDescriptor + DownloaderDescriptor = descriptor.DownloaderDescriptor + DownloaderKey = descriptor.DownloaderKey + UploaderDescriptor = descriptor.UploaderDescriptor + UploaderKey = descriptor.UploaderKey + UploaderKeySet = descriptor.UploaderKeySet + ValueSetDefinition = descriptor.ValueSetDefinition + ValueSetDescriptor = descriptor.ValueSetDescriptor + CommandDescriptor = descriptor.CommandDescriptor + + AccessSpecInfo = internal.AccessSpecInfo + UploadTargetSpecInfo = internal.UploadTargetSpecInfo +) diff --git a/api/ocm/plugin/internal/access.go b/api/ocm/plugin/internal/access.go new file mode 100644 index 000000000..a4238a653 --- /dev/null +++ b/api/ocm/plugin/internal/access.go @@ -0,0 +1,12 @@ +package internal + +import ( + "ocm.software/ocm/api/credentials" +) + +type AccessSpecInfo struct { + Short string `json:"short"` + MediaType string `json:"mediaType"` + Hint string `json:"hint"` + ConsumerId credentials.ConsumerIdentity `json:"consumerId"` +} diff --git a/pkg/contexts/ocm/plugin/internal/action.go b/api/ocm/plugin/internal/action.go similarity index 100% rename from pkg/contexts/ocm/plugin/internal/action.go rename to api/ocm/plugin/internal/action.go diff --git a/api/ocm/plugin/internal/upload.go b/api/ocm/plugin/internal/upload.go new file mode 100644 index 000000000..495cc1aee --- /dev/null +++ b/api/ocm/plugin/internal/upload.go @@ -0,0 +1,9 @@ +package internal + +import ( + "ocm.software/ocm/api/credentials" +) + +type UploadTargetSpecInfo struct { + ConsumerId credentials.ConsumerIdentity `json:"consumerId"` +} diff --git a/pkg/contexts/ocm/plugin/internal/valueset.go b/api/ocm/plugin/internal/valueset.go similarity index 100% rename from pkg/contexts/ocm/plugin/internal/valueset.go rename to api/ocm/plugin/internal/valueset.go diff --git a/api/ocm/plugin/plugin.go b/api/ocm/plugin/plugin.go new file mode 100644 index 000000000..f546a0781 --- /dev/null +++ b/api/ocm/plugin/plugin.go @@ -0,0 +1,445 @@ +package plugin + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "sync" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/finalizer" + "github.com/mandelsoft/vfs/pkg/vfs" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/credentials/cpi" + "ocm.software/ocm/api/credentials/identity/hostpath" + "ocm.software/ocm/api/datacontext/attrs/clicfgattr" + "ocm.software/ocm/api/ocm" + "ocm.software/ocm/api/ocm/plugin/cache" + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/compose" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/get" + accval "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/validate" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/action" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/action/execute" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/command" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/download" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler" + merge "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler/execute" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/put" + uplval "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/validate" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset" + vscompose "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/compose" + vsval "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/validate" + "ocm.software/ocm/api/ocm/valuemergehandler" + "ocm.software/ocm/api/utils/cobrautils/flagsets" + "ocm.software/ocm/api/utils/cobrautils/logopts/logging" + "ocm.software/ocm/api/utils/runtime" +) + +type Plugin = *pluginImpl + +type impl = cache.Plugin + +// //nolint: errname // is no error. +type pluginImpl struct { + lock sync.RWMutex + ctx ocm.Context + impl + config json.RawMessage + disableAutoConfiguration bool +} + +func NewPlugin(ctx ocm.Context, impl cache.Plugin, config json.RawMessage) Plugin { + return &pluginImpl{ + ctx: ctx, + impl: impl, + config: config, + } +} + +func (p *pluginImpl) Context() ocm.Context { + return p.ctx +} + +func (p *pluginImpl) DisableAutoConfiguration(flag bool) { + p.disableAutoConfiguration = flag +} + +func (p *pluginImpl) IsAutoConfigurationEnabled() bool { + return !p.disableAutoConfiguration +} + +func (p *pluginImpl) SetConfig(config json.RawMessage) { + p.lock.Lock() + defer p.lock.Unlock() + p.config = config +} + +func (p *pluginImpl) Exec(r io.Reader, w io.Writer, args ...string) (result []byte, rerr error) { + var ( + finalize finalizer.Finalizer + err error + logfile *os.File + ) + + defer finalize.FinalizeWithErrorPropagationf(&rerr, "error processing plugin command %s", args[0]) + + if p.GetDescriptor().ForwardLogging { + logfile, err = os.CreateTemp("", "ocm-plugin-log-*") + if rerr != nil { + return nil, err + } + logfile.Close() + finalize.With(func() error { + return os.Remove(logfile.Name()) + }, "failed to remove temporary log file %s", logfile.Name()) + + lcfg := &logging.LoggingConfiguration{} + _, err = p.Context().ConfigContext().ApplyTo(0, lcfg) + if err != nil { + return nil, errors.Wrapf(err, "cannot extract plugin logging configration") + } + lcfg.LogFileName = logfile.Name() + data, err := json.Marshal(lcfg) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal plugin logging configration") + } + args = append([]string{"--" + ppi.OptPlugingLogConfig, string(data)}, args...) + } + + if len(p.config) == 0 { + p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args) + } else { + p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args, "config", p.config) + } + data, err := cache.Exec(p.Path(), p.config, r, w, args...) + + if logfile != nil { + r, oerr := os.OpenFile(logfile.Name(), vfs.O_RDONLY, 0o600) + if oerr == nil { + finalize.Close(r, "plugin logfile", logfile.Name()) + w := p.ctx.LoggingContext().Tree().LogWriter() + if w == nil { + if logging.GlobalLogFile != nil { + w = logging.GlobalLogFile.File() + } + if w == nil { + w = os.Stderr + } + } + + // weaken the sync problem when merging log files. + // If a SyncWriter is used, the copy is done under a write lock. + // This is only a solution, if the log records are written + // by single write calls. + // The underlying logging apis do not expose their + // sync mechanism for writing log records. + if writer, ok := w.(io.ReaderFrom); ok { + writer.ReadFrom(r) + } else { + io.Copy(w, r) + } + } + } + return data, err +} + +func (p *pluginImpl) MergeValue(specification *valuemergehandler.Specification, local, inbound valuemergehandler.Value) (bool, *valuemergehandler.Value, error) { + desc := p.GetValueMappingDescriptor(specification.Algorithm) + if desc == nil { + return false, nil, errors.ErrNotSupported(valuemergehandler.KIND_VALUE_MERGE_ALGORITHM, specification.Algorithm, KIND_PLUGIN, p.Name()) + } + input, err := json.Marshal(ppi.ValueMergeData{ + Local: local, + Inbound: inbound, + }) + if err != nil { + return false, nil, err + } + + args := []string{mergehandler.Name, merge.Name, specification.Algorithm} + if len(specification.Config) > 0 { + args = append(args, string(specification.Config)) + } + + var buf bytes.Buffer + _, err = p.Exec(bytes.NewReader(input), &buf, args...) + if err != nil { + return false, nil, errors.Wrapf(err, "plugin %s", p.Name()) + } + var r ppi.ValueMergeResult + + err = json.Unmarshal(buf.Bytes(), &r) + if err != nil { + if r.Message != "" { + return false, nil, fmt.Errorf("%w: %s", err, r.Message) + } + return false, nil, err + } + return r.Modified, &r.Value, nil +} + +func (p *pluginImpl) Action(spec ppi.ActionSpec, creds json.RawMessage) (ppi.ActionResult, error) { + desc := p.GetActionDescriptor(spec.GetKind()) + if desc == nil { + return nil, errors.ErrNotSupported(KIND_ACTION, spec.GetKind(), KIND_PLUGIN, p.Name()) + } + if desc.ConsumerType != "" { + cid := spec.GetConsumerAttributes() + cid[cpi.ID_TYPE] = desc.ConsumerType + c, err := credentials.CredentialsForConsumer(p.Context(), credentials.ConsumerIdentity(cid), hostpath.Matcher) + if err != nil || c == nil { + return nil, errors.ErrNotFound(credentials.KIND_CREDENTIALS, cid.String()) + } + creds, err = json.Marshal(c.Properties()) + if err != nil { + return nil, errors.Wrapf(err, "cannot marshal credentials") + } + } + + data, err := p.ctx.GetActions().GetActionTypes().EncodeActionSpec(spec, runtime.DefaultJSONEncoding) + if err != nil { + return nil, err + } + + args := []string{action.Name, execute.Name, string(data)} + if creds != nil { + args = append(args, "--"+get.OptCreds, string(creds)) + } + + result, err := p.Exec(nil, nil, args...) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s", p.Name()) + } + + info, err := p.ctx.GetActions().GetActionTypes().DecodeActionResult(result, runtime.DefaultJSONEncoding) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s: cannot unmarshal action result", p.Name()) + } + return info, nil +} + +func (p *pluginImpl) ValidateAccessMethod(spec []byte) (*ppi.AccessSpecInfo, error) { + result, err := p.Exec(nil, nil, accessmethod.Name, accval.Name, string(spec)) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s", p.Name()) + } + + var info ppi.AccessSpecInfo + err = json.Unmarshal(result, &info) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s: cannot unmarshal access spec info", p.Name()) + } + return &info, nil +} + +func (p *pluginImpl) ComposeAccessMethod(name string, opts flagsets.ConfigOptions, base flagsets.Config) error { + cfg := flagsets.Config{} + for _, o := range opts.Options() { + cfg[o.GetName()] = o.Value() + } + optsdata, err := json.Marshal(cfg) + if err != nil { + return errors.Wrapf(err, "cannot marshal option values") + } + basedata, err := json.Marshal(base) + if err != nil { + return errors.Wrapf(err, "cannot marshal access specification base value") + } + result, err := p.Exec(nil, nil, accessmethod.Name, compose.Name, name, string(optsdata), string(basedata)) + if err != nil { + return err + } + var r flagsets.Config + err = json.Unmarshal(result, &r) + if err != nil { + return errors.Wrapf(err, "cannot unmarshal composition result") + } + + for k := range base { + delete(base, k) + } + for k, v := range r { + base[k] = v + } + return nil +} + +func (p *pluginImpl) ValidateUploadTarget(name string, spec []byte) (*ppi.UploadTargetSpecInfo, error) { + result, err := p.Exec(nil, nil, upload.Name, uplval.Name, name, string(spec)) + if err != nil { + return nil, errors.Wrapf(err, "plugin uploader %s/%s", p.Name(), name) + } + + var info ppi.UploadTargetSpecInfo + err = json.Unmarshal(result, &info) + if err != nil { + return nil, errors.Wrapf(err, "plugin uploader %s/%s: cannot unmarshal upload target info", p.Name(), name) + } + return &info, nil +} + +func (p *pluginImpl) Get(w io.Writer, creds, spec json.RawMessage) error { + args := []string{accessmethod.Name, get.Name, string(spec)} + if creds != nil { + args = append(args, "--"+get.OptCreds, string(creds)) + } + _, err := p.Exec(nil, w, args...) + return err +} + +func (p *pluginImpl) Put(name string, r io.Reader, artType, mimeType, hint string, creds, target json.RawMessage) (ocm.AccessSpec, error) { + args := []string{upload.Name, put.Name, name, string(target)} + + if creds != nil { + args = append(args, "--"+put.OptCreds, string(creds)) + } + if hint != "" { + args = append(args, "--"+put.OptHint, hint) + } + if mimeType != "" { + args = append(args, "--"+put.OptMedia, mimeType) + } + if artType != "" { + args = append(args, "--"+put.OptArt, artType) + } + result, err := p.Exec(r, nil, args...) + if err != nil { + return nil, err + } + var m map[string]interface{} + err = json.Unmarshal(result, &m) + if err != nil { + return nil, errors.Wrapf(err, "cannot unmarshal put result") + } + if len(m) == 0 { + return nil, nil // not used + } + return p.ctx.AccessSpecForConfig(result, runtime.DefaultJSONEncoding) +} + +func (p *pluginImpl) Download(name string, r io.Reader, artType, mimeType, target string, config json.RawMessage) (bool, string, error) { + args := []string{download.Name, name, target} + + if mimeType != "" { + args = append(args, "--"+download.OptMedia, mimeType) + } + if artType != "" { + args = append(args, "--"+download.OptArt, artType) + } + + // new attribute can only be set for extended plugin format version + // so, omitting config if not set is compatible with former CLI. + if d := p.GetDescriptor().Downloaders.Get(name); len(config) > 0 && d != nil && d.ConfigScheme != "" { + args = append(args, "--"+download.OptConfig, string(config)) + } + result, err := p.Exec(r, nil, args...) + if err != nil { + return true, "", err + } + var m download.Result + err = json.Unmarshal(result, &m) + if err != nil { + return true, "", errors.Wrapf(err, "cannot unmarshal put result") + } + if m.Error != "" { + return true, "", fmt.Errorf("%s", m.Error) + } + return m.Path != "", m.Path, nil +} + +func (p *pluginImpl) ValidateValueSet(purpose string, spec []byte) (*ppi.ValueSetInfo, error) { + result, err := p.Exec(nil, nil, valueset.Name, vsval.Name, purpose, string(spec)) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s", p.Name()) + } + + var info ppi.ValueSetInfo + err = json.Unmarshal(result, &info) + if err != nil { + return nil, errors.Wrapf(err, "plugin %s: cannot unmarshal value set info", p.Name()) + } + return &info, nil +} + +func (p *pluginImpl) ComposeValueSet(purpose, name string, opts flagsets.ConfigOptions, base flagsets.Config) error { + cfg := flagsets.Config{} + for _, o := range opts.Options() { + cfg[o.GetName()] = o.Value() + } + optsdata, err := json.Marshal(cfg) + if err != nil { + return errors.Wrapf(err, "cannot marshal option values") + } + basedata, err := json.Marshal(base) + if err != nil { + return errors.Wrapf(err, "cannot marshal access specification base value") + } + result, err := p.Exec(nil, nil, valueset.Name, vscompose.Name, purpose, name, string(optsdata), string(basedata)) + if err != nil { + return err + } + var r flagsets.Config + err = json.Unmarshal(result, &r) + if err != nil { + return errors.Wrapf(err, "cannot unmarshal composition result") + } + + for k := range base { + delete(base, k) + } + for k, v := range r { + base[k] = v + } + return nil +} + +func (p *pluginImpl) Command(name string, reader io.Reader, writer io.Writer, cmdargs []string) (rerr error) { + var finalize finalizer.Finalizer + cmd := p.GetDescriptor().Commands.Get(name) + if cmd == nil { + return errors.ErrNotFound("command", name) + } + + defer finalize.FinalizeWithErrorPropagation(&rerr) + + var f vfs.File + + args := []string{command.Name} + + a := clicfgattr.Get(p.Context()) + if a != nil && cmd.CLIConfigRequired { + cfgdata, err := json.Marshal(a) + if err != nil { + return errors.Wrapf(err, "cannot marshal CLI config") + } + // cannot use a vfs here, since it's not possible to pass it to the plugin + f, err = os.CreateTemp("", "cli-om-config-*") + if err != nil { + return err + } + finalize.With(func() error { + return os.Remove(f.Name()) + }, "failed to remove temporary config file %s", f.Name()) + + _, err = f.Write(cfgdata) + if err != nil { + f.Close() + return err + } + err = f.Close() + if err != nil { + return err + } + args = append(args, "--"+command.OptCliConfig, f.Name()) + } + args = append(append(args, name), cmdargs...) + + _, err := p.Exec(reader, writer, args...) + return err +} diff --git a/pkg/contexts/ocm/plugin/plugin_test.go b/api/ocm/plugin/plugin_test.go similarity index 79% rename from pkg/contexts/ocm/plugin/plugin_test.go rename to api/ocm/plugin/plugin_test.go index 5851d5e1f..89e76b66e 100644 --- a/pkg/contexts/ocm/plugin/plugin_test.go +++ b/api/ocm/plugin/plugin_test.go @@ -6,22 +6,22 @@ import ( . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/testutils" - - common2 "github.com/open-component-model/ocm/pkg/common" - "github.com/open-component-model/ocm/pkg/contexts/credentials" - "github.com/open-component-model/ocm/pkg/contexts/oci/actions/oci-repository-prepare" - "github.com/open-component-model/ocm/pkg/contexts/ocm" - access "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/plugin" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugincacheattr" - "github.com/open-component-model/ocm/pkg/contexts/ocm/attrs/plugindirattr" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/common" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/plugins" - "github.com/open-component-model/ocm/pkg/contexts/ocm/registration" - "github.com/open-component-model/ocm/pkg/contexts/ocm/valuemergehandler" - "github.com/open-component-model/ocm/pkg/contexts/ocm/valuemergehandler/handlers/defaultmerge" + . "ocm.software/ocm/api/ocm/plugin/testutils" + + "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/oci/extensions/actions/oci-repository-prepare" + "ocm.software/ocm/api/ocm" + access "ocm.software/ocm/api/ocm/extensions/accessmethods/plugin" + "ocm.software/ocm/api/ocm/extensions/attrs/plugincacheattr" + "ocm.software/ocm/api/ocm/extensions/attrs/plugindirattr" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/plugin/cache" + "ocm.software/ocm/api/ocm/plugin/common" + "ocm.software/ocm/api/ocm/plugin/plugins" + "ocm.software/ocm/api/ocm/plugin/registration" + "ocm.software/ocm/api/ocm/valuemergehandler" + "ocm.software/ocm/api/ocm/valuemergehandler/handlers/defaultmerge" + common2 "ocm.software/ocm/api/utils/misc" ) var _ = Describe("setup plugin cache", func() { diff --git a/pkg/contexts/ocm/plugin/plugins/plugins.go b/api/ocm/plugin/plugins/plugins.go similarity index 82% rename from pkg/contexts/ocm/plugin/plugins/plugins.go rename to api/ocm/plugin/plugins/plugins.go index 7772c610d..53e20d2f0 100644 --- a/pkg/contexts/ocm/plugin/plugins/plugins.go +++ b/api/ocm/plugin/plugins/plugins.go @@ -4,13 +4,13 @@ import ( "encoding/json" "sync" - cfgcpi "github.com/open-component-model/ocm/pkg/contexts/config/cpi" - "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/cache" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/config" - "github.com/open-component-model/ocm/pkg/contexts/ocm/plugin/descriptor" - "github.com/open-component-model/ocm/pkg/utils" + cfgcpi "ocm.software/ocm/api/config/cpi" + "ocm.software/ocm/api/ocm/cpi" + "ocm.software/ocm/api/ocm/plugin" + "ocm.software/ocm/api/ocm/plugin/cache" + "ocm.software/ocm/api/ocm/plugin/config" + "ocm.software/ocm/api/ocm/plugin/descriptor" + "ocm.software/ocm/api/utils" ) type Set = *pluginsImpl diff --git a/pkg/contexts/ocm/plugin/ppi/clicmd/options.go b/api/ocm/plugin/ppi/clicmd/options.go similarity index 100% rename from pkg/contexts/ocm/plugin/ppi/clicmd/options.go rename to api/ocm/plugin/ppi/clicmd/options.go diff --git a/api/ocm/plugin/ppi/clicmd/utils.go b/api/ocm/plugin/ppi/clicmd/utils.go new file mode 100644 index 000000000..437e0e4bc --- /dev/null +++ b/api/ocm/plugin/ppi/clicmd/utils.go @@ -0,0 +1,85 @@ +package clicmd + +import ( + _ "ocm.software/ocm/cmds/ocm/clippi/config" + + "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/optionutils" + "github.com/spf13/cobra" + + "ocm.software/ocm/api/ocm/plugin/ppi" +) + +//////////////////////////////////////////////////////////////////////////////// + +type CobraCommand struct { + cmd *cobra.Command + verb string + realm string + objname string + cliConfigRequired bool +} + +var _ ppi.Command = (*CobraCommand)(nil) + +// NewCLICommand created a CLI command based on a preconfigured cobra.Command. +// Optionally, a verb can be specified. If given additionally a realm +// can be given. +// verb and realm are used to add the command at the appropriate places in +// the command hierarchy of the ocm CLI. +// If nothing is specified, the command will be a new top-level command. +// To access the configured ocm context use the Context attribute +// of the cobra command. The ocm context is bound to it. +// +// ocm.FromContext(cmd.Context()) +func NewCLICommand(cmd *cobra.Command, opts ...Option) (ppi.Command, error) { + eff := optionutils.EvalOptions(opts...) + if eff.Verb == "" && eff.Realm != "" { + return nil, errors.New("realm without verb not allowed") + } + cmd.DisableFlagsInUseLine = true + return &CobraCommand{cmd, eff.Verb, eff.Realm, eff.ObjectType, optionutils.AsBool(eff.RequireCLIConfig, false)}, nil +} + +func (c *CobraCommand) Name() string { + return c.cmd.Name() +} + +func (c *CobraCommand) Description() string { + return c.cmd.Long +} + +func (c *CobraCommand) Usage() string { + return c.cmd.Use +} + +func (c *CobraCommand) Short() string { + return c.cmd.Short +} + +func (c *CobraCommand) Example() string { + return c.cmd.Example +} + +func (c *CobraCommand) ObjectType() string { + if c.objname == "" { + return c.Name() + } + return c.objname +} + +func (c *CobraCommand) Verb() string { + return c.verb +} + +func (c *CobraCommand) Realm() string { + return c.realm +} + +func (c *CobraCommand) CLIConfigRequired() bool { + return c.cliConfigRequired +} + +func (c *CobraCommand) Command() *cobra.Command { + return c.cmd +} diff --git a/api/ocm/plugin/ppi/cmds/accessmethod/cmd.go b/api/ocm/plugin/ppi/cmds/accessmethod/cmd.go new file mode 100644 index 000000000..7ca09d52e --- /dev/null +++ b/api/ocm/plugin/ppi/cmds/accessmethod/cmd.go @@ -0,0 +1,26 @@ +package accessmethod + +import ( + "github.com/spf13/cobra" + + "ocm.software/ocm/api/ocm/plugin/ppi" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/compose" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/get" + "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod/validate" +) + +const Name = "accessmethod" + +func New(p ppi.Plugin) *cobra.Command { + cmd := &cobra.Command{ + Use: Name, + Short: "access method operations", + Long: `This command group provides all commands used to implement an access method +described by an access method descriptor (
name
field should be defined for an option. If required, new options can be
+defined by additionally specifying a type and a description. New options should
+be used very carefully. The chosen names MUST not conflict with names provided
+by other plugins. Therefore, it is highly recommended to use names prefixed
+by the plugin name.
+
+` + options.DefaultRegistry.Usage(),
+ Args: cobra.ExactArgs(3),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Name string
+ Options ppi.Config
+ Base ppi.Config
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+}
+
+func (o *Options) Complete(args []string) error {
+ o.Name = args[0]
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[1]), &o.Options); err != nil {
+ return errors.Wrapf(err, "invalid access specification options")
+ }
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[2]), &o.Base); err != nil {
+ return errors.Wrapf(err, "invalid base access specification")
+ }
+ return nil
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ k, v := runtime.KindVersion(opts.Name)
+ m := p.GetAccessMethod(k, v)
+ if m == nil {
+ return errors.ErrUnknown(errkind.KIND_ACCESSMETHOD, opts.Name)
+ }
+ err := opts.Options.ConvertFor(m.Options()...)
+ if err != nil {
+ return err
+ }
+ err = m.ComposeAccessSpecification(p, opts.Options, opts.Base)
+ if err != nil {
+ return err
+ }
+ data, err := json.Marshal(opts.Base)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go b/api/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go
new file mode 100644
index 000000000..3eb3870bd
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/accessmethod/get/cmd.go
@@ -0,0 +1,87 @@
+package get
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ commonppi "ocm.software/ocm/api/ocm/plugin/ppi/cmds/common"
+ "ocm.software/ocm/api/utils/cobrautils/flag"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Name = "get"
+ OptCreds = commonppi.OptCreds
+)
+
+func New(p ppi.Plugin) *cobra.Command {
+ opts := Options{}
+
+ cmd := &cobra.Command{
+ Use: Name + " [mediaType
** *string*
+
+ The media type of the artifact described by the specification. It may be part
+ of the specification or implicitly determined by the access method.
+
+- **description
** *string*
+
+ A short textual description of the described location.
+
+- **hint
** *string*
+
+ A name hint of the described location used to reconstruct a useful
+ name for local blobs uploaded to a dedicated repository technology.
+
+- **consumerId
** *map[string]string*
+
+ The consumer id used to determine optional credentials for the
+ underlying repository. If specified, at least the type
field must be set.
+`,
+ Args: cobra.ExactArgs(1),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Specification json.RawMessage
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+}
+
+func (o *Options) Complete(args []string) error {
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[0]), &o.Specification); err != nil {
+ return errors.Wrapf(err, "invalid access specification")
+ }
+ return nil
+}
+
+type Result struct {
+ MediaType string `json:"mediaType"`
+ Short string `json:"description"`
+ Hint string `json:"hint"`
+ ConsumerId credentials.ConsumerIdentity `json:"consumerId"`
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ spec, err := p.DecodeAccessSpecification(opts.Specification)
+ if err != nil {
+ return errors.Wrapf(err, "access specification")
+ }
+
+ m := p.GetAccessMethod(runtime.KindVersion(spec.GetType()))
+ if m == nil {
+ return errors.ErrUnknown(errkind.KIND_ACCESSMETHOD, spec.GetType())
+ }
+ info, err := m.ValidateSpecification(p, spec)
+ if err != nil {
+ return err
+ }
+ result := Result{MediaType: info.MediaType, ConsumerId: info.ConsumerId, Hint: info.Hint, Short: info.Short}
+ data, err := json.Marshal(result)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/cmds/action/cmd.go b/api/ocm/plugin/ppi/cmds/action/cmd.go
new file mode 100644
index 000000000..9e1ac14b4
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/action/cmd.go
@@ -0,0 +1,21 @@
+package action
+
+import (
+ "github.com/spf13/cobra"
+
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/action/execute"
+)
+
+const Name = "action"
+
+func New(p ppi.Plugin) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: Name,
+ Short: "action operations",
+ Long: `This command group provides all commands used to implement an action.`,
+ }
+
+ cmd.AddCommand(execute.New(p))
+ return cmd
+}
diff --git a/api/ocm/plugin/ppi/cmds/action/execute/cmd.go b/api/ocm/plugin/ppi/cmds/action/execute/cmd.go
new file mode 100644
index 000000000..50e09da69
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/action/execute/cmd.go
@@ -0,0 +1,97 @@
+package execute
+
+import (
+ "encoding/json"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext/action"
+ "ocm.software/ocm/api/datacontext/action/api"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/common"
+ "ocm.software/ocm/api/utils/cobrautils/flag"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Name = "execute"
+ OptCreds = common.OptCreds
+)
+
+func New(p ppi.Plugin) *cobra.Command {
+ opts := Options{}
+
+ cmd := &cobra.Command{
+ Use: Name + " name
** *string*
+
+ The name and version of the action result. It must match the value
+ from the action specification.
+
+- **message
** *string*
+
+ An error message.
+
+Additional fields depend on the kind of action.
+`,
+ Args: cobra.ExactArgs(1),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Credentials credentials.DirectCredentials
+ Specification json.RawMessage
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+ flag.YAMLVarP(fs, &o.Credentials, OptCreds, "c", nil, "credentials")
+ flag.StringToStringVarPFA(fs, &o.Credentials, "credential", "C", nil, "dedicated credential value")
+}
+
+func (o *Options) Complete(args []string) error {
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[0]), &o.Specification); err != nil {
+ return errors.Wrapf(err, "invalid access specification")
+ }
+ return nil
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ spec, err := action.DefaultRegistry().DecodeActionSpec(opts.Specification, runtime.DefaultJSONEncoding)
+ if err != nil {
+ return errors.Wrapf(err, "action specification")
+ }
+
+ a := p.GetAction(spec.GetKind())
+ if a == nil {
+ return errors.ErrUnknown(api.KIND_ACTION, spec.GetKind())
+ }
+ result, err := a.Execute(p, spec, opts.Credentials)
+ if err != nil {
+ return err
+ }
+ result.SetType(spec.GetType())
+ data, err := action.DefaultRegistry().EncodeActionResult(result, runtime.DefaultJSONEncoding)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/cmds/app.go b/api/ocm/plugin/ppi/cmds/app.go
new file mode 100644
index 000000000..b2129bfef
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/app.go
@@ -0,0 +1,106 @@
+//go:generate go run -mod=mod ./doc ../../../../../docs/pluginreference
+
+package cmds
+
+import (
+ "encoding/json"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/accessmethod"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/action"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/command"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/describe"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/download"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/info"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/mergehandler"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/topics/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset"
+ "ocm.software/ocm/api/utils/cobrautils"
+)
+
+type PluginCommand struct {
+ command *cobra.Command
+ plugin ppi.Plugin
+}
+
+func (p *PluginCommand) Command() *cobra.Command {
+ return p.command
+}
+
+func NewPluginCommand(p ppi.Plugin) *PluginCommand {
+ short := p.Descriptor().Short
+ if short == "" {
+ short = "OCM plugin " + p.Name()
+ }
+
+ pcmd := &PluginCommand{
+ plugin: p,
+ }
+ cmd := &cobra.Command{
+ Use: p.Name() + " local
** *any*
+
+ The local value to merge into the inbound value.
+
+- **inbound
** *any*
+
+ The value to merge into. This value is based on the original inbound value.
+
+This action has to provide an execution result as JSON string on *stdout*. It has the
+following fields:
+
+- **modified
** *bool*
+
+ Whether the inbound value has been modified by merging with the local value.
+
+- **value
** *string*
+
+ The merged value
+
+- **message
** *string*
+
+ An error message.
+`,
+ Args: cobra.ExactArgs(2),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Name string
+ Configuration json.RawMessage
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+}
+
+func (o *Options) Complete(args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("algorithm name missing")
+ }
+ o.Name = args[0]
+ if len(args) > 1 {
+ o.Configuration = []byte(args[1])
+ }
+ if len(args) > 2 {
+ return fmt.Errorf("too many arguments")
+ }
+ return nil
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ h := p.GetValueMergeHandler(opts.Name)
+ if h == nil {
+ return errors.ErrUnknown(hpi.KIND_VALUE_MERGE_ALGORITHM, opts.Name)
+ }
+
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ var input ppi.ValueMergeData
+ err = json.Unmarshal(data, &input)
+ if err != nil {
+ return err
+ }
+
+ result, err := h.Execute(p, input.Local, input.Inbound, opts.Configuration)
+ if err != nil {
+ return err
+ }
+ data, err = runtime.DefaultJSONEncoding.Marshal(result)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/pkg/contexts/ocm/plugin/ppi/cmds/topics/descriptor/topic.go b/api/ocm/plugin/ppi/cmds/topics/descriptor/topic.go
similarity index 99%
rename from pkg/contexts/ocm/plugin/ppi/cmds/topics/descriptor/topic.go
rename to api/ocm/plugin/ppi/cmds/topics/descriptor/topic.go
index 0f92d562d..417ebe828 100644
--- a/pkg/contexts/ocm/plugin/ppi/cmds/topics/descriptor/topic.go
+++ b/api/ocm/plugin/ppi/cmds/topics/descriptor/topic.go
@@ -3,7 +3,7 @@ package descriptor
import (
"github.com/spf13/cobra"
- "github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/options"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
)
func New() *cobra.Command {
diff --git a/api/ocm/plugin/ppi/cmds/upload/cmd.go b/api/ocm/plugin/ppi/cmds/upload/cmd.go
new file mode 100644
index 000000000..2196ee853
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/upload/cmd.go
@@ -0,0 +1,25 @@
+package upload
+
+import (
+ "github.com/spf13/cobra"
+
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/put"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/upload/validate"
+)
+
+const Name = "upload"
+
+func New(p ppi.Plugin) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: Name,
+ Short: "upload specific operations",
+ Long: `
+This command group provides all commands used to implement an uploader
+described by an uploader descriptor.`,
+ }
+
+ cmd.AddCommand(validate.New(p))
+ cmd.AddCommand(put.New(p))
+ return cmd
+}
diff --git a/api/ocm/plugin/ppi/cmds/upload/put/cmd.go b/api/ocm/plugin/ppi/cmds/upload/put/cmd.go
new file mode 100644
index 000000000..7bdac8633
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/upload/put/cmd.go
@@ -0,0 +1,110 @@
+package put
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/common"
+ "ocm.software/ocm/api/utils/cobrautils/flag"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const (
+ Name = "put"
+ OptCreds = common.OptCreds
+ OptHint = common.OptHint
+ OptMedia = common.OptMedia
+ OptArt = common.OptArt
+)
+
+func New(p ppi.Plugin) *cobra.Command {
+ opts := Options{}
+
+ cmd := &cobra.Command{
+ Use: Name + " [consumerId
** *map[string]string*
+
+ The consumer id used to determine optional credentials for the
+ underlying repository. If specified, at least the type
field must
+ be set.
+`,
+ Args: cobra.ExactArgs(2),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Name string
+ Specification json.RawMessage
+
+ ArtifactType string
+ MediaType string
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+ fs.StringVarP(&o.MediaType, OptMedia, "m", "", "media type of input blob")
+ fs.StringVarP(&o.ArtifactType, OptArt, "a", "", "artifact type of input blob")
+}
+
+func (o *Options) Complete(args []string) error {
+ o.Name = args[0]
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[1]), &o.Specification); err != nil {
+ return errors.Wrapf(err, "invalid repository specification")
+ }
+ return nil
+}
+
+type Result struct {
+ ConsumerId credentials.ConsumerIdentity `json:"consumerId"`
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ spec, err := p.DecodeUploadTargetSpecification(opts.Specification)
+ if err != nil {
+ return errors.Wrapf(err, "target specification")
+ }
+
+ m := p.GetUploader(opts.Name)
+ if m == nil {
+ return errors.ErrUnknown(descriptor.KIND_UPLOADER, spec.GetType())
+ }
+ info, err := m.ValidateSpecification(p, spec)
+ if err != nil {
+ return err
+ }
+ result := Result{info.ConsumerId}
+ data, err := json.Marshal(result)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/cmds/valueset/cmd.go b/api/ocm/plugin/ppi/cmds/valueset/cmd.go
new file mode 100644
index 000000000..86d77ec3b
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/valueset/cmd.go
@@ -0,0 +1,24 @@
+package valueset
+
+import (
+ "github.com/spf13/cobra"
+
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/compose"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/valueset/validate"
+)
+
+const Name = "valueset"
+
+func New(p ppi.Plugin) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: Name,
+ Short: "valueset operations",
+ Long: `This command group provides all commands used to implement a value set
+described by a value set descriptor (name
field should be defined for an option. If required, new options can be
+defined by additionally specifying a type and a description. New options should
+be used very carefully. The chosen names MUST not conflict with names provided
+by other plugins. Therefore, it is highly recommended to use names prefixed
+by the plugin name.
+
+` + options.DefaultRegistry.Usage(),
+ Args: cobra.ExactArgs(4),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Purpose string
+ Name string
+ Options ppi.Config
+ Base ppi.Config
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+}
+
+func (o *Options) Complete(args []string) error {
+ o.Purpose = args[0]
+ o.Name = args[1]
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[2]), &o.Options); err != nil {
+ return errors.Wrapf(err, "invalid avalue set options")
+ }
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[3]), &o.Base); err != nil {
+ return errors.Wrapf(err, "invalid base set specification")
+ }
+ return nil
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ k, v := runtime.KindVersion(opts.Name)
+ s := p.GetValueSet(opts.Purpose, k, v)
+ if s == nil {
+ return errors.ErrUnknown(descriptor.KIND_VALUESET, opts.Name)
+ }
+ err := opts.Options.ConvertFor(s.Options()...)
+ if err != nil {
+ return err
+ }
+ err = s.ComposeSpecification(p, opts.Options, opts.Base)
+ if err != nil {
+ return err
+ }
+ data, err := json.Marshal(opts.Base)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/cmds/valueset/validate/cmd.go b/api/ocm/plugin/ppi/cmds/valueset/validate/cmd.go
new file mode 100644
index 000000000..a1ad30ea6
--- /dev/null
+++ b/api/ocm/plugin/ppi/cmds/valueset/validate/cmd.go
@@ -0,0 +1,89 @@
+package validate
+
+import (
+ "encoding/json"
+
+ "github.com/mandelsoft/goutils/errors"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/ppi"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+const Name = "validate"
+
+func New(p ppi.Plugin) *cobra.Command {
+ opts := Options{}
+
+ cmd := &cobra.Command{
+ Use: Name + " description
** *string*
+
+ A short textual description of the described value set.
+`,
+ Args: cobra.ExactArgs(2),
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return opts.Complete(args)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return Command(p, cmd, &opts)
+ },
+ }
+ opts.AddFlags(cmd.Flags())
+ return cmd
+}
+
+type Options struct {
+ Purpose string
+ Specification json.RawMessage
+}
+
+func (o *Options) AddFlags(fs *pflag.FlagSet) {
+}
+
+func (o *Options) Complete(args []string) error {
+ o.Purpose = args[0]
+ if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[1]), &o.Specification); err != nil {
+ return errors.Wrapf(err, "invalid valueset specification")
+ }
+ return nil
+}
+
+type Result struct {
+ Short string `json:"description"`
+}
+
+func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
+ spec, err := p.DecodeValueSet(opts.Purpose, opts.Specification)
+ if err != nil {
+ return errors.Wrapf(err, "access specification")
+ }
+
+ k, v := runtime.KindVersion(spec.GetType())
+ m := p.GetValueSet(opts.Purpose, k, v)
+ if m == nil {
+ return errors.ErrUnknown(descriptor.KIND_VALUESET, spec.GetType())
+ }
+ info, err := m.ValidateSpecification(p, spec)
+ if err != nil {
+ return err
+ }
+ result := Result{Short: info.Short}
+ data, err := json.Marshal(result)
+ if err != nil {
+ return err
+ }
+ cmd.Printf("%s\n", string(data))
+ return nil
+}
diff --git a/api/ocm/plugin/ppi/config/config.go b/api/ocm/plugin/ppi/config/config.go
new file mode 100644
index 000000000..b1b1e8262
--- /dev/null
+++ b/api/ocm/plugin/ppi/config/config.go
@@ -0,0 +1,28 @@
+package config
+
+import (
+ "context"
+
+ "ocm.software/ocm/api/ocm"
+ "ocm.software/ocm/api/ocm/plugin/ppi/cmds/command"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+func init() {
+ command.RegisterCommandConfigHandler(&commandHandler{})
+}
+
+type commandHandler struct{}
+
+func (c commandHandler) HandleConfig(ctx context.Context, data []byte) (context.Context, error) {
+ var err error
+
+ octx := ocm.DefaultContext()
+ ctx = octx.BindTo(ctx)
+ if len(data) != 0 {
+ _, err = octx.ConfigContext().ApplyData(data, runtime.DefaultYAMLEncoding, " cli config")
+ // Ugly, enforce configuration update
+ octx.GetResolver()
+ }
+ return ctx, err
+}
diff --git a/pkg/contexts/ocm/plugin/ppi/config/doc.go b/api/ocm/plugin/ppi/config/doc.go
similarity index 100%
rename from pkg/contexts/ocm/plugin/ppi/config/doc.go
rename to api/ocm/plugin/ppi/config/doc.go
diff --git a/pkg/contexts/ocm/plugin/ppi/doc.go b/api/ocm/plugin/ppi/doc.go
similarity index 100%
rename from pkg/contexts/ocm/plugin/ppi/doc.go
rename to api/ocm/plugin/ppi/doc.go
diff --git a/api/ocm/plugin/ppi/interface.go b/api/ocm/plugin/ppi/interface.go
new file mode 100644
index 000000000..c0a590576
--- /dev/null
+++ b/api/ocm/plugin/ppi/interface.go
@@ -0,0 +1,213 @@
+package ppi
+
+import (
+ "encoding/json"
+ "io"
+
+ "github.com/spf13/cobra"
+
+ "ocm.software/ocm/api/config/cpi"
+ "ocm.software/ocm/api/credentials"
+ "ocm.software/ocm/api/datacontext/action"
+ "ocm.software/ocm/api/ocm/extensions/accessmethods/options"
+ "ocm.software/ocm/api/ocm/plugin/descriptor"
+ "ocm.software/ocm/api/ocm/plugin/internal"
+ "ocm.software/ocm/api/utils/runtime"
+)
+
+type (
+ Descriptor = descriptor.Descriptor
+ UploaderKey = descriptor.UploaderKey
+ UploaderDescriptor = descriptor.UploaderDescriptor
+ DownloaderKey = descriptor.DownloaderKey
+ DownloaderDescriptor = descriptor.DownloaderDescriptor
+ AccessMethodDescriptor = descriptor.AccessMethodDescriptor
+ CLIOption = descriptor.CLIOption
+
+ ActionSpecInfo = internal.ActionSpecInfo
+ AccessSpecInfo = internal.AccessSpecInfo
+ ValueSetInfo = internal.ValueSetInfo
+ UploadTargetSpecInfo = internal.UploadTargetSpecInfo
+)
+
+var REALM = descriptor.REALM
+
+type Plugin interface {
+ Name() string
+ Version() string
+ Descriptor() descriptor.Descriptor
+
+ SetDescriptorTweaker(func(descriptor descriptor.Descriptor) descriptor.Descriptor)
+
+ SetShort(s string)
+ SetLong(s string)
+ SetConfigParser(config func(raw json.RawMessage) (interface{}, error))
+ ForwardLogging(b ...bool)
+
+ RegisterDownloader(arttype, mediatype string, u Downloader) error
+ GetDownloader(name string) Downloader
+ GetDownloaderFor(arttype, mediatype string) Downloader
+
+ RegisterUploader(arttype, mediatype string, u Uploader) error
+ GetUploader(name string) Uploader
+ GetUploaderFor(arttype, mediatype string) Uploader
+ DecodeUploadTargetSpecification(data []byte) (UploadTargetSpec, error)
+
+ RegisterAccessMethod(m AccessMethod) error
+ DecodeAccessSpecification(data []byte) (AccessSpec, error)
+ GetAccessMethod(name string, version string) AccessMethod
+
+ RegisterAction(a Action) error
+ DecodeAction(data []byte) (ActionSpec, error)
+ GetAction(name string) Action
+
+ RegisterValueMergeHandler(h ValueMergeHandler) error
+ GetValueMergeHandler(name string) ValueMergeHandler
+
+ RegisterValueSet(h ValueSet) error
+ DecodeValueSet(purpose string, data []byte) (runtime.TypedObject, error)
+ GetValueSet(purpose, name, version string) ValueSet
+
+ RegisterCommand(c Command) error
+ GetCommand(name string) Command
+ Commands() []Command
+
+ RegisterConfigType(c cpi.ConfigType) error
+ GetConfigType(name string) *descriptor.ConfigTypeDescriptor
+ ConfigTypes() []descriptor.ConfigTypeDescriptor
+
+ GetOptions() *Options
+ GetConfig() (interface{}, error)
+}
+
+type AccessMethod interface {
+ runtime.TypedObjectDecoder[AccessSpec]
+
+ Name() string
+ Version() string
+
+ // Options provides the list of CLI options supported to compose the access
+ // specification.
+ Options() []options.OptionType
+
+ // Description provides a general description for the access mehod kind.
+ Description() string
+ // Format describes the attributes of the dedicated version.
+ Format() string
+
+ ValidateSpecification(p Plugin, spec AccessSpec) (info *AccessSpecInfo, err error)
+ Reader(p Plugin, spec AccessSpec, creds credentials.Credentials) (io.ReadCloser, error)
+ ComposeAccessSpecification(p Plugin, opts Config, config Config) error
+}
+
+type AccessSpec = runtime.TypedObject
+
+type AccessSpecProvider func() AccessSpec
+
+type UploadFormats runtime.KnownTypes[runtime.TypedObject, runtime.TypedObjectDecoder[runtime.TypedObject]]
+
+type Uploader interface {
+ Decoders() UploadFormats
+
+ Name() string
+ Description() string
+
+ ValidateSpecification(p Plugin, spec UploadTargetSpec) (info *UploadTargetSpecInfo, err error)
+ Writer(p Plugin, arttype, mediatype string, hint string, spec UploadTargetSpec, creds credentials.Credentials) (io.WriteCloser, AccessSpecProvider, error)
+}
+
+type UploadTargetSpec = runtime.TypedObject
+
+type DownloadResultProvider func() (string, error)
+
+type Downloader interface {
+ Name() string
+ Description() string
+ ConfigSchema() []byte
+
+ Writer(p Plugin, arttype, mediatype string, filepath string, config []byte) (io.WriteCloser, DownloadResultProvider, error)
+}
+
+type ActionSpec = action.ActionSpec
+
+type ActionResult = action.ActionResult
+
+type Action interface {
+ Name() string
+ Description() string
+ DefaultSelectors() []string
+ ConsumerType() string
+
+ Execute(p Plugin, spec ActionSpec, creds credentials.DirectCredentials) (result ActionResult, err error)
+}
+
+type Value = runtime.RawValue
+
+type ValueMergeResult struct {
+ Modified bool `json:"modified"`
+ Value Value `json:"value"`
+ Message string `json:"message,omitempty"`
+}
+
+type ValueMergeData struct {
+ Local Value `json:"local"`
+ Inbound Value `json:"inbound"`
+}
+
+type ValueMergeHandler interface {
+ Name() string
+ Description() string
+
+ Execute(p Plugin, local Value, inbound Value, config json.RawMessage) (result ValueMergeResult, err error)
+}
+
+type ValueSet interface {
+ runtime.TypedObjectDecoder[AccessSpec]
+
+ Name() string
+ Version() string
+
+ // Purposes describes the purposes the set should be ued for.
+ // So far, only the purpose PURPOSE_ROUTINGSLIP is defined.
+ Purposes() []string
+
+ // Options provides the list of CLI options supported to compose the access
+ // specification.
+ Options() []options.OptionType
+
+ // Description provides a general description for the access mehod kind.
+ Description() string
+ // Format describes the attributes of the dedicated version.
+ Format() string
+
+ ValidateSpecification(p Plugin, spec runtime.TypedObject) (info *ValueSetInfo, err error)
+ ComposeSpecification(p Plugin, opts Config, config Config) error
+}
+
+// Command is the interface for a CLI command provided by a plugin.
+type Command interface {
+ // Name of command used in the plugin.
+ // This is also the default object type and is used to
+ // name top-level commands in the CLI.
+ Name() string
+ Description() string
+ Usage() string
+ Short() string
+ Example() string
+ // ObjectType is optional and can be used
+ // together with a verb. It then is used as
+ // sub command name for the object type.
+ // By default, the command name is used.
+ ObjectType() string
+ // Verb is optional and can be set
+ // to place the command in the verb hierarchy of
+ // the OCM CLI. It is used together with the ObjectType.
+ // (command will be *ocm