diff --git a/api/krm.go b/api/krm.go index 2514093..c3bd907 100644 --- a/api/krm.go +++ b/api/krm.go @@ -76,7 +76,7 @@ func (i *KRMInput) GetRemoteClient(items []*kyaml.RNode) (*auth.Client, error) { } } - reference, err := i.RemoteModule.GetReference() + reference, err := i.RemoteModule.ParseReference() if err != nil { return nil, fmt.Errorf("failed to get reference: %w", err) } diff --git a/api/remote_module.go b/api/remote_module.go index a7543d7..bdbc7c7 100644 --- a/api/remote_module.go +++ b/api/remote_module.go @@ -31,7 +31,7 @@ type RemoteModule struct { PlainHTTP bool `yaml:"plainHTTP,omitempty" json:"plainHTTP,omitempty"` } -func (r *RemoteModule) GetReference() (registry.Reference, error) { +func (r *RemoteModule) ParseReference() (registry.Reference, error) { if r.Ref != "" { return registry.ParseReference(r.Ref) } diff --git a/api/remote_module_test.go b/api/remote_module_test.go index cbf61dc..274889e 100644 --- a/api/remote_module_test.go +++ b/api/remote_module_test.go @@ -103,7 +103,7 @@ func TestRemoteModule_GetReference(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ref, err := tt.module.GetReference() + ref, err := tt.module.ParseReference() if tt.wantErr { require.Error(t, err) @@ -124,7 +124,7 @@ func TestRemoteModule_BackwardsCompatibility(t *testing.T) { Tag: "v1.0.0", } - ref, err := module.GetReference() + ref, err := module.ParseReference() require.NoError(t, err, "GetReference() unexpected error = %v", err) assert.Equal(t, "ghcr.io", ref.Registry) @@ -140,7 +140,7 @@ func TestRemoteModule_BackwardsCompatibility(t *testing.T) { Tag: "v1.0.0", } - ref, err := module.GetReference() + ref, err := module.ParseReference() require.NoError(t, err) assert.Equal(t, "new-registry.io", ref.Registry) diff --git a/go.mod b/go.mod index 431c855..9f8156e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Workday/cuestomize -go 1.26.0 +go 1.26.4 require ( cuelang.org/go v0.16.1 @@ -8,14 +8,23 @@ require ( github.com/stretchr/testify v1.11.1 k8s.io/api v0.36.2 k8s.io/apimachinery v0.36.2 - k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 - oras.land/oras-go/v2 v2.6.0 + k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 + oras.land/oras-go/v2 v2.6.1 sigs.k8s.io/kustomize/api v0.21.1 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) require ( + cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/proto v1.14.3 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect github.com/go-openapi/swag/cmdutils v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/fileutils v0.25.4 // indirect @@ -27,19 +36,6 @@ require ( github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect -) - -require ( - cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect - github.com/cockroachdb/apd/v3 v3.2.1 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/proto v1.14.3 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/jsonreference v0.21.4 // indirect - github.com/go-openapi/swag v0.25.4 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -60,11 +56,11 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -72,4 +68,5 @@ require ( k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/go.sum b/go.sum index d180d2b..af859cd 100644 --- a/go.sum +++ b/go.sum @@ -118,20 +118,20 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -148,12 +148,12 @@ k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 h1:mPMaPMpBij2V1Wv/fR+HW124vVGXXvOSS9ver/9yjWs= -k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 h1:m2wZhD5+vJZyCVkTvUHIfaiXc/mdt3Pxyx3vUnGsKzU= +k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= +oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= diff --git a/internal/pkg/cuestomize/function.go b/internal/pkg/cuestomize/function.go index bbb7aaa..aed54a5 100644 --- a/internal/pkg/cuestomize/function.go +++ b/internal/pkg/cuestomize/function.go @@ -30,7 +30,7 @@ func newCuestomizeFunctionWithPath(ctx context.Context, config *api.KRMInput, re var provider model.Provider if config.RemoteModule != nil { - ociProvider, err := model.NewOCIModelProviderFromConfigAndItems(config, items, model.WithWorkingDir(*resourcesPath)) + ociProvider, err := model.NewOCIModelProviderFromConfigAndItems(config, items, model.WithWorkingDir(*resourcesPath), model.WithUnpackArchivePostFetch()) if err != nil { return nil, err } diff --git a/pkg/cuestomize/model/oci_provider.go b/pkg/cuestomize/model/oci_provider.go index 91b6b88..4e25fdc 100644 --- a/pkg/cuestomize/model/oci_provider.go +++ b/pkg/cuestomize/model/oci_provider.go @@ -18,12 +18,15 @@ import ( // OCIOption defines a functional option for configuring OCIModelProvider. type OCIOption func(*ociModelProviderOptions) +type postFetchFunc = func(ctx context.Context, p *OCIModelProvider) error + // ociModelProviderOptions holds configuration options for OCIModelProvider. type ociModelProviderOptions struct { - Reference registry.Reference - PlainHTTP bool - Client *auth.Client - WorkingDir string + Reference registry.Reference + PlainHTTP bool + Client *auth.Client + WorkingDir string + postFetchFunc postFetchFunc } // WithRemoteParts configures the OCI remote to fetch the CUE model from an OCI registry. @@ -70,15 +73,64 @@ func WithClient(client *auth.Client) OCIOption { } } +// WithPostFetchFunc configures a post-fetch function that will be called after the CUE model is fetched from the OCI registry. This can be used to perform +// additional processing on the fetched artifact. +func WithPostFetchFunc(postFetchFunc postFetchFunc) OCIOption { + return func(opts *ociModelProviderOptions) { + opts.postFetchFunc = postFetchFunc + } +} + +// WithUnpackArchivePostFetch configures a post-fetch function that checks if the fetched artifact is a compressed tarball and, if so, decompresses it in place. +// If the artifact is not a compressed tarball, this function does nothing. This is a best-effort attempt to support both plain directories and tarballs artifacts. +func WithUnpackArchivePostFetch() OCIOption { + return WithPostFetchFunc(func(ctx context.Context, p *OCIModelProvider) error { + log := logr.FromContextOrDiscard(ctx) + + // check if we pulled a compressed tarball and if so, attempt to decompress it in place + // this is a best effort attempt to support both plain directories and compressed tarballs as OCI artifacts + // without requiring users to specify the format of the artifact in the configuration + entries, err := os.ReadDir(p.workingDir) + if err != nil { + return fmt.Errorf("failed to read working directory: %w", err) + } + + log.Info("fetched CUE model from OCI registry", "entries", func() []string { + names := make([]string, len(entries)) + for i, entry := range entries { + names[i] = entry.Name() + } + return names + }()) + + if len(entries) == 1 && !entries[0].IsDir() && files.IsArchive(entries[0].Name()) { + archivePath := filepath.Join(p.workingDir, entries[0].Name()) + wdir, err := filepath.Abs(p.workingDir) + if err != nil { + return fmt.Errorf("failed to get absolute path of working directory: %w", err) + } + log.Info("detected archive, attempting to decompress", "archive", archivePath) + err = files.Untar(archivePath, wdir, files.RemoveArchive(true)) + if err != nil { + return fmt.Errorf("failed to decompress archive: %w", err) + } + } + + return nil + }) +} + // OCIModelProvider is a model provider that fetches the CUE model from an OCI registry. type OCIModelProvider struct { - reference registry.Reference - plainHTTP bool - workingDir string - client *auth.Client + reference registry.Reference + plainHTTP bool + workingDir string + client *auth.Client + postFetchFunc postFetchFunc } -// NewOCIModelProviderFromConfigAndItems creates a new OCIModelProvider based on the provided KRMInput configuration and input items. +// NewOCIModelProviderFromConfigAndItems creates a new OCIModelProvider based on the provided KRMInput configuration and options. +// Options can be used to override default behavior, such as the working directory, post-fetch processing, etc. func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml.RNode, opts ...OCIOption) (*OCIModelProvider, error) { if config.RemoteModule == nil { return nil, fmt.Errorf("remote module configuration is missing") @@ -88,7 +140,7 @@ func NewOCIModelProviderFromConfigAndItems(config *api.KRMInput, items []*kyaml. return nil, fmt.Errorf("failed to configure remote client: %w", err) } - reference, err := config.RemoteModule.GetReference() + reference, err := config.RemoteModule.ParseReference() if err != nil { return nil, fmt.Errorf("failed to get reference: %w", err) } @@ -120,10 +172,11 @@ func New(opts ...OCIOption) (*OCIModelProvider, error) { } return &OCIModelProvider{ - reference: options.Reference, - plainHTTP: options.PlainHTTP, - workingDir: options.WorkingDir, - client: options.Client, + reference: options.Reference, + plainHTTP: options.PlainHTTP, + workingDir: options.WorkingDir, + client: options.Client, + postFetchFunc: options.postFetchFunc, }, nil } @@ -151,32 +204,9 @@ func (p *OCIModelProvider) Get(ctx context.Context) error { return fmt.Errorf("failed to fetch from OCI registry: %w", err) } - // check if we pulled a compressed tarball and if so, attempt to decompress it in place - // this is a best effort attempt to support both plain directories and compressed tarballs as OCI artifacts - // without requiring users to specify the format of the artifact in the configuration - entries, err := os.ReadDir(p.workingDir) - if err != nil { - return fmt.Errorf("failed to read working directory: %w", err) - } - - log.Info("fetched CUE model from OCI registry", "entries", func() []string { - names := make([]string, len(entries)) - for i, entry := range entries { - names[i] = entry.Name() - } - return names - }()) - - if len(entries) == 1 && !entries[0].IsDir() && files.IsArchive(entries[0].Name()) { - archivePath := filepath.Join(p.workingDir, entries[0].Name()) - wdir, err := filepath.Abs(p.workingDir) - if err != nil { - return fmt.Errorf("failed to get absolute path of working directory: %w", err) - } - log.Info("detected archive, attempting to decompress", "archive", archivePath) - err = files.Untar(archivePath, wdir, files.RemoveArchive(true)) - if err != nil { - return fmt.Errorf("failed to decompress archive: %w", err) + if p.postFetchFunc != nil { + if err := p.postFetchFunc(ctx, p); err != nil { + return fmt.Errorf("post-fetch function failed: %w", err) } } diff --git a/pkg/oci/fetcher/fetcher.go b/pkg/oci/fetcher/fetcher.go index 0e25e0c..426826c 100644 --- a/pkg/oci/fetcher/fetcher.go +++ b/pkg/oci/fetcher/fetcher.go @@ -25,6 +25,7 @@ func FetchFromOCIRegistry(ctx context.Context, client remote.Client, workingDir if err != nil { return err } + if client != nil { repository.Client = client } diff --git a/semver b/semver index 48080b4..70a6ac9 100644 --- a/semver +++ b/semver @@ -1 +1 @@ -v0.5.0 \ No newline at end of file +v0.5.1-alpha.1 \ No newline at end of file