mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
feat(registry): add --subject flag for OCI Referrers API
Allow associating a chart with a container image via OCI Referrers API. When pushing a chart with --subject, the manifest will include a subject field pointing to the specified digest. Example: helm push chart.tgz oci://registry/repo --subject sha256:abc... This enables workflows where Helm charts are published as referrers to container images, allowing registry clients to discover related artifacts through the OCI Referrers API. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
This commit is contained in:
parent
812eb526e0
commit
ae3c2777e5
7 changed files with 52 additions and 9 deletions
|
|
@ -38,6 +38,7 @@ type Push struct {
|
|||
insecureSkipTLSVerify bool
|
||||
plainHTTP bool
|
||||
out io.Writer
|
||||
subject string
|
||||
}
|
||||
|
||||
// PushOpt is a type of function that sets options for a push action.
|
||||
|
|
@ -80,6 +81,14 @@ func WithPushOptWriter(out io.Writer) PushOpt {
|
|||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the subject digest for OCI Referrers API.
|
||||
// When set, the pushed chart will be associated with the specified image digest.
|
||||
func WithSubject(subject string) PushOpt {
|
||||
return func(p *Push) {
|
||||
p.subject = subject
|
||||
}
|
||||
}
|
||||
|
||||
// NewPushWithOpts creates a new push, with configuration options.
|
||||
func NewPushWithOpts(opts ...PushOpt) *Push {
|
||||
p := &Push{}
|
||||
|
|
@ -100,6 +109,7 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
|
|||
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
|
||||
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
|
||||
pusher.WithPlainHTTP(p.plainHTTP),
|
||||
pusher.WithSubject(p.subject),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ type registryPushOptions struct {
|
|||
plainHTTP bool
|
||||
password string
|
||||
username string
|
||||
subject string
|
||||
}
|
||||
|
||||
func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
|
|
@ -84,7 +85,8 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
|||
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
|
||||
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
|
||||
action.WithPlainHTTP(o.plainHTTP),
|
||||
action.WithPushOptWriter(out))
|
||||
action.WithPushOptWriter(out),
|
||||
action.WithSubject(o.subject))
|
||||
client.Settings = settings
|
||||
output, err := client.Run(chartRef, remote)
|
||||
if err != nil {
|
||||
|
|
@ -103,6 +105,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
|||
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
|
||||
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart")
|
||||
f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")
|
||||
f.StringVar(&o.subject, "subject", "", "associate chart with a container image via OCI Referrers API (digest format: sha256:...)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"helm.sh/helm/v4/internal/tlsutil"
|
||||
"helm.sh/helm/v4/pkg/chart/v2/loader"
|
||||
"helm.sh/helm/v4/pkg/registry"
|
||||
|
|
@ -93,6 +96,14 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
|
|||
chartArchiveFileCreatedTime := stat.ModTime()
|
||||
pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339)))
|
||||
|
||||
// Add subject for OCI Referrers API if specified
|
||||
if pusher.opts.subject != "" {
|
||||
subjectDesc := &ocispec.Descriptor{
|
||||
Digest: digest.Digest(pusher.opts.subject),
|
||||
}
|
||||
pushOpts = append(pushOpts, registry.PushOptSubject(subjectDesc))
|
||||
}
|
||||
|
||||
_, err = client.Push(chartBytes, ref, pushOpts...)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type options struct {
|
|||
caFile string
|
||||
insecureSkipTLSVerify bool
|
||||
plainHTTP bool
|
||||
subject string
|
||||
}
|
||||
|
||||
// Option allows specifying various settings configurable by the user for overriding the defaults
|
||||
|
|
@ -69,6 +70,14 @@ func WithPlainHTTP(plainHTTP bool) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the subject digest for OCI Referrers API.
|
||||
// When set, the pushed chart will be associated with the specified image digest.
|
||||
func WithSubject(subject string) Option {
|
||||
return func(opts *options) {
|
||||
opts.subject = subject
|
||||
}
|
||||
}
|
||||
|
||||
// Pusher is an interface to support upload to the specified URL.
|
||||
type Pusher interface {
|
||||
// Push file content by url string
|
||||
|
|
|
|||
|
|
@ -639,6 +639,7 @@ type (
|
|||
provData []byte
|
||||
strictMode bool
|
||||
creationTime string
|
||||
subject *ocispec.Descriptor
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -703,7 +704,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
|
|||
ociAnnotations := generateOCIAnnotations(meta, operation.creationTime)
|
||||
|
||||
manifestDescriptor, err := c.tagManifest(ctx, memoryStore, configDescriptor,
|
||||
layers, ociAnnotations, parsedRef)
|
||||
layers, ociAnnotations, parsedRef, operation.subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -775,6 +776,13 @@ func PushOptCreationTime(creationTime string) PushOption {
|
|||
}
|
||||
}
|
||||
|
||||
// PushOptSubject returns a function that sets the subject for Referrers API
|
||||
func PushOptSubject(subject *ocispec.Descriptor) PushOption {
|
||||
return func(operation *pushOperation) {
|
||||
operation.subject = subject
|
||||
}
|
||||
}
|
||||
|
||||
// Tags provides a sorted list all semver compliant tags for a given repository
|
||||
func (c *Client) Tags(ref string) ([]string, error) {
|
||||
parsedReference, err := registry.ParseReference(ref)
|
||||
|
|
@ -910,7 +918,8 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *ur
|
|||
// tagManifest prepares and tags a manifest in memory storage
|
||||
func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store,
|
||||
configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor,
|
||||
ociAnnotations map[string]string, parsedRef reference) (ocispec.Descriptor, error) {
|
||||
ociAnnotations map[string]string, parsedRef reference,
|
||||
subject *ocispec.Descriptor) (ocispec.Descriptor, error) {
|
||||
|
||||
manifest := ocispec.Manifest{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
|
|
@ -918,6 +927,7 @@ func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store,
|
|||
Config: configDescriptor,
|
||||
Layers: layers,
|
||||
Annotations: ociAnnotations,
|
||||
Subject: subject,
|
||||
}
|
||||
|
||||
manifestData, err := json.Marshal(manifest)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func TestTagManifestTransformsReferences(t *testing.T) {
|
|||
parsedRef, err := newReference(refWithPlus)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc, err := client.tagManifest(ctx, memStore, configDesc, layers, nil, parsedRef)
|
||||
desc, err := client.tagManifest(ctx, memStore, configDesc, layers, nil, parsedRef, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
transformedDesc, err := memStore.Resolve(ctx, expectedRef)
|
||||
|
|
|
|||
|
|
@ -278,12 +278,12 @@ func testPush(suite *TestRegistry) {
|
|||
suite.Equal(ref, result.Ref)
|
||||
suite.Equal(meta.Name, result.Chart.Meta.Name)
|
||||
suite.Equal(meta.Version, result.Chart.Meta.Version)
|
||||
suite.Equal(int64(742), result.Manifest.Size)
|
||||
suite.Equal(int64(800), result.Manifest.Size)
|
||||
suite.Equal(int64(99), result.Config.Size)
|
||||
suite.Equal(int64(973), result.Chart.Size)
|
||||
suite.Equal(int64(695), result.Prov.Size)
|
||||
suite.Equal(
|
||||
"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
|
||||
"sha256:bc20397d31b1236b50d506e960b7ea81137712a88d084d3bddeb18a386797af9",
|
||||
result.Manifest.Digest)
|
||||
suite.Equal(
|
||||
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
|
||||
|
|
@ -351,12 +351,12 @@ func testPull(suite *TestRegistry) {
|
|||
suite.Equal(ref, result.Ref)
|
||||
suite.Equal(meta.Name, result.Chart.Meta.Name)
|
||||
suite.Equal(meta.Version, result.Chart.Meta.Version)
|
||||
suite.Equal(int64(742), result.Manifest.Size)
|
||||
suite.Equal(int64(800), result.Manifest.Size)
|
||||
suite.Equal(int64(99), result.Config.Size)
|
||||
suite.Equal(int64(973), result.Chart.Size)
|
||||
suite.Equal(int64(695), result.Prov.Size)
|
||||
suite.Equal(
|
||||
"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
|
||||
"sha256:bc20397d31b1236b50d506e960b7ea81137712a88d084d3bddeb18a386797af9",
|
||||
result.Manifest.Digest)
|
||||
suite.Equal(
|
||||
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
|
||||
|
|
@ -367,7 +367,7 @@ func testPull(suite *TestRegistry) {
|
|||
suite.Equal(
|
||||
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
|
||||
result.Prov.Digest)
|
||||
suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
|
||||
suite.Equal("{\"schemaVersion\":2,\"artifactType\":\"application/vnd.cncf.helm.config.v1+json\",\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
|
||||
string(result.Manifest.Data))
|
||||
suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
|
||||
string(result.Config.Data))
|
||||
|
|
|
|||
Loading…
Reference in a new issue