Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/krm.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ func (i *KRMInput) GetRemoteClient(items []*kyaml.RNode) (*auth.Client, error) {
}
}

return registryauth.ConfigureClient(i.RemoteModule.Registry, secret)
reference, err := i.RemoteModule.GetReference()
if err != nil {
return nil, fmt.Errorf("failed to get reference: %w", err)
}
return registryauth.ConfigureClient(reference.Registry, secret)
}

// ItemMatchReference checks if the given item matches the provided selector.
Expand Down
38 changes: 34 additions & 4 deletions api/remote_module.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
package api

import "sigs.k8s.io/kustomize/api/types"
import (
"fmt"

"oras.land/oras-go/v2/registry"
"sigs.k8s.io/kustomize/api/types"
)

// RemoteModule defines the structure to describe a remote CUE module to fetch from an OCI registry.
type RemoteModule struct {
Registry string `yaml:"registry" json:"registry"`
Repo string `yaml:"repo" json:"repo"`
Tag string `yaml:"tag" json:"tag"`
// Ref is the full OCI reference in the format: registry/repo:tag
// Example: ghcr.io/workday/my-module:v1.0.0
// If Ref is specified, it takes precedence over Registry, Repo, and Tag.
Ref string `yaml:"ref,omitempty" json:"ref,omitempty"`

// Registry is the OCI registry hosting the module.
//
// Deprecated: Use Ref instead. Registry will be removed in a future version.
Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`
// Repo is the repository path within the OCI registry.
//
// Deprecated: Use Ref instead. Repo will be removed in a future version.
Repo string `yaml:"repo,omitempty" json:"repo,omitempty"`
// Tag is the version tag of the module.
//
// Deprecated: Use Ref instead. Tag will be removed in a future version.
Tag string `yaml:"tag,omitempty" json:"tag,omitempty"`

Auth *types.Selector `yaml:"auth,omitempty" json:"auth,omitempty"`
PlainHTTP bool `yaml:"plainHTTP,omitempty" json:"plainHTTP,omitempty"`
}

func (r *RemoteModule) GetReference() (registry.Reference, error) {
if r.Ref != "" {
return registry.ParseReference(r.Ref)
}
referenceStr := fmt.Sprintf("%s/%s", r.Registry, r.Repo)
if r.Tag != "" {
referenceStr = fmt.Sprintf("%s:%s", referenceStr, r.Tag)
}
return registry.ParseReference(referenceStr)
}
150 changes: 150 additions & 0 deletions api/remote_module_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package api

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRemoteModule_GetReference(t *testing.T) {
tests := []struct {
name string
module RemoteModule
wantRegistry string
wantRepo string
wantRef string
wantErr bool
}{
{
name: "uses ref when present",
module: RemoteModule{
Ref: "ghcr.io/workday/module:v1.0.0",
Registry: "old-registry.io",
Repo: "old-repo",
Tag: "old-tag",
},
wantRegistry: "ghcr.io",
wantRepo: "workday/module",
wantRef: "v1.0.0",
wantErr: false,
},
{
name: "constructs from separate fields when ref is empty",
module: RemoteModule{
Registry: "docker.io",
Repo: "library/nginx",
Tag: "latest",
},
wantRegistry: "docker.io",
wantRepo: "library/nginx",
wantRef: "latest",
wantErr: false,
},
{
name: "handles nested repo paths",
module: RemoteModule{
Ref: "registry.io/org/team/project:v2.0.0",
},
wantRegistry: "registry.io",
wantRepo: "org/team/project",
wantRef: "v2.0.0",
wantErr: false,
},
{
name: "handles registry with port",
module: RemoteModule{
Ref: "registry:5000/sample-module:latest",
},
wantRegistry: "registry:5000",
wantRepo: "sample-module",
wantRef: "latest",
wantErr: false,
},
{
name: "handles localhost registry",
module: RemoteModule{
Ref: "localhost:5000/my-module:v1.0.0",
},
wantRegistry: "localhost:5000",
wantRepo: "my-module",
wantRef: "v1.0.0",
wantErr: false,
},
{
name: "constructs without tag defaults to empty",
module: RemoteModule{
Registry: "ghcr.io",
Repo: "workday/module",
},
wantRegistry: "ghcr.io",
wantRepo: "workday/module",
wantRef: "",
wantErr: false,
},
{
name: "handles sha256 tags",
module: RemoteModule{
Ref: "docker.io/nginx:sha256-abc123",
},
wantRegistry: "docker.io",
wantRepo: "nginx",
wantRef: "sha256-abc123",
wantErr: false,
},
{
name: "returns error for invalid ref",
module: RemoteModule{
Ref: "invalid",
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, err := tt.module.GetReference()

if tt.wantErr {
require.Error(t, err)
} else {
assert.Equal(t, tt.wantRegistry, ref.Registry)
assert.Equal(t, tt.wantRepo, ref.Repository)
assert.Equal(t, tt.wantRef, ref.Reference)
}
})
}
}

func TestRemoteModule_BackwardsCompatibility(t *testing.T) {
t.Run("deprecated fields still work", func(t *testing.T) {
module := RemoteModule{
Registry: "ghcr.io",
Repo: "workday/module",
Tag: "v1.0.0",
}

ref, err := module.GetReference()
require.NoError(t, err, "GetReference() unexpected error = %v", err)

assert.Equal(t, "ghcr.io", ref.Registry)
assert.Equal(t, "workday/module", ref.Repository)
assert.Equal(t, "v1.0.0", ref.Reference)
})

t.Run("ref takes precedence over deprecated fields", func(t *testing.T) {
module := RemoteModule{
Ref: "new-registry.io/new-repo:v2.0.0",
Registry: "old-registry.io",
Repo: "old-repo",
Tag: "v1.0.0",
}

ref, err := module.GetReference()
require.NoError(t, err)

assert.Equal(t, "new-registry.io", ref.Registry)
assert.Equal(t, "new-repo", ref.Repository)
assert.Equal(t, "v2.0.0", ref.Reference)
})
}
51 changes: 32 additions & 19 deletions pkg/cuestomize/model/oci_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/Workday/cuestomize/api"
"github.com/Workday/cuestomize/pkg/oci/fetcher"
"github.com/go-logr/logr"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote/auth"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
)
Expand All @@ -18,20 +19,32 @@ type OCIOption func(*ociModelProviderOptions)

// ociModelProviderOptions holds configuration options for OCIModelProvider.
type ociModelProviderOptions struct {
Registry string
Repo string
Tag string
Reference registry.Reference
PlainHTTP bool
Client *auth.Client
WorkingDir string
}

// WithRemote configures the OCI remote to fetch the CUE model from.
func WithRemote(registry, repo, tag string) OCIOption {
// WithRemoteParts configures the OCI remote to fetch the CUE model from an OCI registry.
//
// Panics if the provided registry, repo, and tag do not form a valid OCI reference.
func WithRemoteParts(reg, repo, tag string) OCIOption {
reference := fmt.Sprintf("%s/%s", reg, repo)
if tag != "" {
reference = fmt.Sprintf("%s:%s", reference, tag)
}
ref, err := registry.ParseReference(reference)
if err != nil {
panic(fmt.Sprintf("invalid reference: %v", err))
}
return func(opts *ociModelProviderOptions) {
opts.Registry = registry
opts.Repo = repo
opts.Tag = tag
opts.Reference = ref
}
}

func WithRemote(ref registry.Reference) OCIOption {
return func(opts *ociModelProviderOptions) {
opts.Reference = ref
}
}

Expand All @@ -58,9 +71,7 @@ func WithClient(client *auth.Client) OCIOption {

// OCIModelProvider is a model provider that fetches the CUE model from an OCI registry.
type OCIModelProvider struct {
registry string
repo string
tag string
reference registry.Reference
plainHTTP bool
workingDir string
client *auth.Client
Expand All @@ -75,8 +86,14 @@ func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml.
if err != nil {
return nil, fmt.Errorf("failed to configure remote client: %w", err)
}

reference, err := config.RemoteModule.GetReference()
if err != nil {
return nil, fmt.Errorf("failed to get reference: %w", err)
}

return New(
WithRemote(config.RemoteModule.Registry, config.RemoteModule.Repo, config.RemoteModule.Tag),
WithRemote(reference),
WithPlainHTTP(config.RemoteModule.PlainHTTP),
WithClient(client),
)
Expand All @@ -102,9 +119,7 @@ func New(opts ...OCIOption) (*OCIModelProvider, error) {
}

return &OCIModelProvider{
registry: options.Registry,
repo: options.Repo,
tag: options.Tag,
reference: options.Reference,
plainHTTP: options.PlainHTTP,
workingDir: options.WorkingDir,
client: options.Client,
Expand All @@ -119,7 +134,7 @@ func (p *OCIModelProvider) Path() string {
// Get fetches the CUE model from the OCI registry and stores it in the working directory.
func (p *OCIModelProvider) Get(ctx context.Context) error {
log := logr.FromContextOrDiscard(ctx).V(4).WithValues(
"registry", p.registry, "repo", p.repo, "tag", p.tag, "workingDir", p.workingDir,
"registry", p.reference.Registry, "repo", p.reference.Repository, "tag", p.reference.Reference, "workingDir", p.workingDir,
)

log.Info("fetching from OCI registry", "plainHTTP", p.plainHTTP)
Expand All @@ -128,9 +143,7 @@ func (p *OCIModelProvider) Get(ctx context.Context) error {
ctx,
p.client,
p.workingDir,
p.registry,
p.repo,
p.tag,
p.reference,
p.plainHTTP,
)
if err != nil {
Expand Down
12 changes: 7 additions & 5 deletions pkg/oci/fetcher/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import (
"github.com/go-logr/logr"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
)

// FetchFromOCIRegistry fetches an artifact from an OCI registry and stores it in the specified working directory.
func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir, reg, repo, tag string, plainHTTP bool) error {
func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir string, ref registry.Reference, plainHTTP bool) error {
log := logr.FromContextOrDiscard(ctx).V(4)

fs, err := file.New(workingDir)
if err != nil {
return fmt.Errorf("failed to create file store: %w", err)
}

repository, err := remote.NewRepository(reg + "/" + repo)
repository, err := remote.NewRepository(ref.String())
if err != nil {
return err
}
Expand All @@ -29,14 +30,15 @@ func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir,
}
repository.PlainHTTP = plainHTTP

desc, err := oras.Copy(ctx, repository, tag, fs, tag, oras.DefaultCopyOptions)
desc, err := oras.Copy(ctx, repository, ref.Reference, fs, ref.Reference, oras.DefaultCopyOptions)
if err != nil {
return err
}

log.Info("fetched artifact from OCI registry",
"reg", reg,
"repo", repo,
"reg", ref.Registry,
"repo", ref.Repository,
"tag", ref.Reference,
"workingDir", workingDir,
"digest", desc.Digest.String(),
"mediaType", desc.MediaType,
Expand Down
7 changes: 6 additions & 1 deletion pkg/oci/fetcher/fetcher_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package fetcher

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

"github.com/Workday/cuestomize/internal/pkg/testhelpers"
"github.com/stretchr/testify/require"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)
Expand Down Expand Up @@ -76,8 +78,11 @@ func Test_FetchFromRegistry(t *testing.T) {
// push testdata/sample-module to the registry
_ = testhelpers.PushDirectoryToOCIRegistryT(t, tc.registryHost+"/"+tc.repo+":"+tc.tag, tc.testdataDir, tc.artifactType, tc.tag, tc.client, tc.plainHTTP)

ref, err := registry.ParseReference(fmt.Sprintf("%s/%s:%s", tc.registryHost, tc.repo, tc.tag))
require.NoError(t, err, "failed to parse OCI reference")

// Fetch the module from the registry
err := FetchFromOCIRegistry(ctx, tc.client, tempDir, tc.registryHost, tc.repo, tc.tag, tc.plainHTTP)
err = FetchFromOCIRegistry(ctx, tc.client, tempDir, ref, tc.plainHTTP)
if !tc.shouldError {
require.NoError(t, err, "failed to fetch module from OCI registry")
// verify that tempDir contains the expected files
Expand Down
2 changes: 1 addition & 1 deletion semver
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.1
0.3.2