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
26 changes: 20 additions & 6 deletions cloudprofilesync/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/go-logr/logr"
"golang.org/x/sync/semaphore"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
Expand Down Expand Up @@ -77,6 +78,7 @@ type Source interface {
}

type OCI struct {
log logr.Logger
repo *remote.Repository
sema *semaphore.Weighted
}
Expand All @@ -89,7 +91,7 @@ type OCIParams struct {
Parallel int64 `json:"parallel"`
}

func NewOCI(params OCIParams, insecure bool) (*OCI, error) {
func NewOCI(params OCIParams, insecure bool, log logr.Logger) (*OCI, error) {
// Create a new OCI repository
repo, err := remote.NewRepository(params.Registry + "/" + params.Repository)
if err != nil {
Expand All @@ -109,6 +111,7 @@ func NewOCI(params OCIParams, insecure bool) (*OCI, error) {
repo.PlainHTTP = insecure

return &OCI{
log: log,
repo: repo,
sema: semaphore.NewWeighted(params.Parallel),
}, nil
Expand All @@ -134,7 +137,7 @@ func (o *OCI) GetVersions(ctx context.Context) ([]SourceImage, error) {
defer o.sema.Release(1)
_, reader, err := o.repo.FetchReference(ctx, tag)
if err != nil {
out <- Result[SourceImage]{err: err}
out <- Result[SourceImage]{err: fmt.Errorf("tag %s: failed to fetch manifest: %w", tag, err)}
return
Comment thread
anton-paulovich marked this conversation as resolved.
}
defer reader.Close()
Expand All @@ -143,12 +146,12 @@ func (o *OCI) GetVersions(ctx context.Context) ([]SourceImage, error) {
}{}
err = json.NewDecoder(reader).Decode(&manifest)
if err != nil {
out <- Result[SourceImage]{err: err}
out <- Result[SourceImage]{err: fmt.Errorf("tag %s: failed to decode manifest: %w", tag, err)}
return
Comment thread
anton-paulovich marked this conversation as resolved.
}
arch, ok := manifest.Annotations["architecture"]
if !ok {
out <- Result[SourceImage]{err: fmt.Errorf("architecture annotation not found in descriptor. tag: %s", tag)}
out <- Result[SourceImage]{err: fmt.Errorf("tag %s: architecture annotation not found", tag)}
return
}
var capabilities gardencorev1beta1.Capabilities
Expand Down Expand Up @@ -177,14 +180,25 @@ func (o *OCI) GetVersions(ctx context.Context) ([]SourceImage, error) {
}

images := []SourceImage{}
errs := []error{}
var skipped []error
var errs []error
for range tags {
result := <-out
if result.err != nil {
errs = append(errs, result.err)
if errors.Is(result.err, context.Canceled) || errors.Is(result.err, context.DeadlineExceeded) {
errs = append(errs, result.err)
} else {
skipped = append(skipped, result.err)
}
continue
Comment thread
anton-paulovich marked this conversation as resolved.
Comment thread
anton-paulovich marked this conversation as resolved.
}
images = append(images, result.value)
}
if len(skipped) > 0 {
o.log.V(1).Info("skipped tags with errors", "count", len(skipped), "errors", errors.Join(skipped...))
}
if len(errs) == 0 && len(images) == 0 && len(tags) > 0 {
return nil, fmt.Errorf("all %d tags were skipped; possible registry issue", len(tags))
}
return images, errors.Join(errs...)
Comment thread
anton-paulovich marked this conversation as resolved.
}
58 changes: 54 additions & 4 deletions cloudprofilesync/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencontainers/image-spec/specs-go"
Expand Down Expand Up @@ -59,7 +60,7 @@ var _ = Describe("OCISource", func() {
Registry: registryAddr,
Repository: "repo",
Parallel: 4,
}, true)
}, true, logr.Discard())
Expect(err).To(Succeed())
versions, err := oci.GetVersions(ctx)
Expect(err).To(Succeed())
Expand Down Expand Up @@ -103,7 +104,7 @@ var _ = Describe("OCISource", func() {
Registry: registryAddr,
Repository: "repo-caps",
Parallel: 4,
}, true)
}, true, logr.Discard())
Expect(err).To(Succeed())
versions, err := oci.GetVersions(ctx)
Expect(err).To(Succeed())
Expand Down Expand Up @@ -148,14 +149,63 @@ var _ = Describe("OCISource", func() {
Registry: registryAddr,
Repository: "repo-legacy",
Parallel: 4,
}, true)
}, true, logr.Discard())
Expect(err).To(Succeed())
versions, err := oci.GetVersions(ctx)
Expect(err).To(Succeed())
Expect(versions).To(HaveLen(1))
Expect(versions[0].Capabilities).To(BeNil())
})

It("skips tags without architecture annotation and returns remaining images", func(ctx SpecContext) {
repo, err := remote.NewRepository(registryAddr + "/repo-missing-arch")
Expect(err).To(Succeed())
repo.PlainHTTP = true

withArch := ocispec.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
Manifests: []ocispec.Descriptor{
{MediaType: ocispec.MediaTypeImageManifest, Size: 0, Digest: ocispec.DescriptorEmptyJSON.Digest},
},
Annotations: map[string]string{
"architecture": "amd64",
},
}
withArchBlob, err := json.Marshal(withArch)
Expect(err).To(Succeed())
withArchDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, withArchBlob)

noArch := ocispec.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
Manifests: []ocispec.Descriptor{
{MediaType: ocispec.MediaTypeImageManifest, Size: 0, Digest: ocispec.DescriptorEmptyJSON.Digest},
},
}
noArchBlob, err := json.Marshal(noArch)
Expect(err).To(Succeed())
noArchDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, noArchBlob)

err = repo.Push(ctx, ocispec.DescriptorEmptyJSON, strings.NewReader("{}"))
Expect(err).To(Succeed())
err = repo.PushReference(ctx, withArchDesc, bytes.NewReader(withArchBlob), "1.0.0")
Expect(err).To(Succeed())
err = repo.Push(ctx, ocispec.DescriptorEmptyJSON, strings.NewReader("{}"))
Expect(err).To(Succeed())
err = repo.PushReference(ctx, noArchDesc, bytes.NewReader(noArchBlob), "1.0.1")
Expect(err).To(Succeed())

oci, err := cloudprofilesync.NewOCI(cloudprofilesync.OCIParams{
Registry: registryAddr,
Repository: "repo-missing-arch",
Parallel: 4,
}, true, logr.Discard())
Expect(err).To(Succeed())
versions, err := oci.GetVersions(ctx)
Expect(err).To(Succeed())
Expect(versions).To(HaveLen(1))
Expect(versions[0].Version).To(Equal("1.0.0"))
})

It("leaves Capabilities nil when feature_set contains no valid values", func(ctx SpecContext) {
repo, err := remote.NewRepository(registryAddr + "/repo-no-valid-features")
Expect(err).To(Succeed())
Expand Down Expand Up @@ -185,7 +235,7 @@ var _ = Describe("OCISource", func() {
Registry: registryAddr,
Repository: "repo-no-valid-features",
Parallel: 4,
}, true)
}, true, logr.Discard())
Expect(err).To(Succeed())
versions, err := oci.GetVersions(ctx)
Expect(err).To(Succeed())
Expand Down
8 changes: 4 additions & 4 deletions controllers/managedcloudprofile_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const (

// OCISourceFactory defines an interface for creating OCI sources.
type OCISourceFactory interface {
Create(params cloudprofilesync.OCIParams, insecure bool) (cloudprofilesync.Source, error)
Create(params cloudprofilesync.OCIParams, insecure bool, log logr.Logger) (cloudprofilesync.Source, error)
}

type RegistryClient interface {
Expand All @@ -52,8 +52,8 @@ func (k *KeppelClient) GetTags(ctx context.Context, registry, repository string)
// DefaultOCISourceFactory is the default implementation of OCISourceFactory.
type DefaultOCISourceFactory struct{}

func (f *DefaultOCISourceFactory) Create(params cloudprofilesync.OCIParams, insecure bool) (cloudprofilesync.Source, error) {
return cloudprofilesync.NewOCI(params, insecure)
func (f *DefaultOCISourceFactory) Create(params cloudprofilesync.OCIParams, insecure bool, log logr.Logger) (cloudprofilesync.Source, error) {
return cloudprofilesync.NewOCI(params, insecure, log)
}

type Reconciler struct {
Expand Down Expand Up @@ -288,7 +288,7 @@ func (r *Reconciler) updateMachineImages(ctx context.Context, log logr.Logger, u
Username: update.Source.OCI.Username,
Password: string(password),
Parallel: 1,
}, update.Source.OCI.Insecure)
}, update.Source.OCI.Insecure, log)
if err != nil {
return fmt.Errorf("failed to initialize OCI source: %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions controllers/managedcloudprofile_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

gardenerv1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/go-logr/logr"
providercfg "github.com/ironcore-dev/gardener-extension-provider-ironcore-metal/pkg/apis/metal/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -49,11 +50,11 @@ func (f *fakeOCISource) GetVersions(ctx context.Context) ([]cloudprofilesync.Sou

type fakeFactory struct{}

func (f *fakeFactory) Create(params cloudprofilesync.OCIParams, insecure bool) (cloudprofilesync.Source, error) {
func (f *fakeFactory) Create(params cloudprofilesync.OCIParams, insecure bool, _ logr.Logger) (cloudprofilesync.Source, error) {
return &fakeOCISource{}, nil
}

func (m *mockOCIFactory) Create(params cloudprofilesync.OCIParams, insecure bool) (cloudprofilesync.Source, error) {
func (m *mockOCIFactory) Create(params cloudprofilesync.OCIParams, insecure bool, _ logr.Logger) (cloudprofilesync.Source, error) {
return m.createFunc(params, insecure)
}

Expand Down
Loading