diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go
index a7369a77..672d3665 100644
--- a/api/v1beta2/imagepolicy_types.go
+++ b/api/v1beta2/imagepolicy_types.go
@@ -105,6 +105,10 @@ type ImagePolicyStatus struct {
// the image repository, when filtered and ordered according to
// the policy.
LatestImage string `json:"latestImage,omitempty"`
+ // LatestDigest is the digest of the latest image stored in the
+ // accompanying LatestImage field.
+ // +optional
+ LatestDigest string `json:"latestDigest,omitempty"`
// ObservedPreviousImage is the observed previous LatestImage. It is used
// to keep track of the previous and current images.
// +optional
diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
index 4e6ec8af..4c09094a 100644
--- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
+++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
@@ -383,6 +383,10 @@ spec:
- type
type: object
type: array
+ latestDigest:
+ description: LatestDigest is the digest of the latest image stored
+ in the accompanying LatestImage field.
+ type: string
latestImage:
description: LatestImage gives the first in the list of images scanned
by the image repository, when filtered and ordered according to
diff --git a/docs/api/image-reflector.md b/docs/api/image-reflector.md
index f4eeceeb..b0b81091 100644
--- a/docs/api/image-reflector.md
+++ b/docs/api/image-reflector.md
@@ -313,6 +313,19 @@ the policy.
+
observedPreviousImage
string
diff --git a/internal/controllers/imagepolicy_controller.go b/internal/controllers/imagepolicy_controller.go
index 4e4b7f77..9cb11912 100644
--- a/internal/controllers/imagepolicy_controller.go
+++ b/internal/controllers/imagepolicy_controller.go
@@ -20,9 +20,11 @@ import (
"context"
"errors"
"fmt"
+ "strings"
"time"
"github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -49,6 +51,7 @@ import (
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-reflector-controller/internal/policy"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
// errAccessDenied is returned when an ImageRepository reference in ImagePolicy
@@ -110,6 +113,7 @@ type ImagePolicyReconciler struct {
ControllerName string
Database DatabaseReader
ACLOptions acl.Options
+ RegistryHelper registry.Helper
patchOptions []patch.Option
}
@@ -258,7 +262,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
}
// Cleanup the last result.
- obj.Status.LatestImage = ""
+ obj.Status.LatestImage, obj.Status.LatestDigest = "", ""
// Get ImageRepository from reference.
repo, err := r.getImageRepository(ctx, obj)
@@ -327,6 +331,14 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
if oldObj.Status.LatestImage != obj.Status.LatestImage {
obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage
}
+
+ if oldObj.Status.LatestImage != obj.Status.LatestImage || obj.Status.LatestDigest == "" {
+ obj.Status.LatestDigest, err = r.fetchDigest(ctx, repo, latest, obj)
+ if err != nil {
+ result, retErr = ctrl.Result{}, fmt.Errorf("failed fetching digest of %s: %w", obj.Status.LatestImage, err)
+ return
+ }
+ }
// Parse the observed previous image if any and extract previous tag. This
// is used to determine image tag update path.
if obj.Status.ObservedPreviousImage != "" {
@@ -348,6 +360,23 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
return
}
+func (r *ImagePolicyReconciler) fetchDigest(ctx context.Context, repo *imagev1.ImageRepository, latest string, obj *imagev1.ImagePolicy) (string, error) {
+ ref := strings.Join([]string{repo.Spec.Image, latest}, ":")
+ tagRef, err := name.ParseReference(ref)
+ if err != nil {
+ return "", fmt.Errorf("failed parsing reference %q: %w", ref, err)
+ }
+ opts, err := r.RegistryHelper.GetAuthOptions(ctx, repo, tagRef)
+ if err != nil {
+ return "", fmt.Errorf("failed to configure authentication options: %w", err)
+ }
+ desc, err := remote.Head(tagRef, opts...)
+ if err != nil {
+ return "", fmt.Errorf("failed fetching descriptor for %q: %w", tagRef.String(), err)
+ }
+ return desc.Digest.String(), nil
+}
+
// getImageRepository tries to fetch an ImageRepository referenced by the given
// ImagePolicy if it's accessible.
func (r *ImagePolicyReconciler) getImageRepository(ctx context.Context, obj *imagev1.ImagePolicy) (*imagev1.ImageRepository, error) {
diff --git a/internal/controllers/imagerepository_controller.go b/internal/controllers/imagerepository_controller.go
index b19ed92a..d5611496 100644
--- a/internal/controllers/imagerepository_controller.go
+++ b/internal/controllers/imagerepository_controller.go
@@ -25,15 +25,12 @@ import (
"strings"
"time"
- "github.com/google/go-containerregistry/pkg/authn"
- "github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
@@ -45,8 +42,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
- "github.com/fluxcd/pkg/oci"
- "github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/patch"
@@ -54,7 +49,7 @@ import (
"github.com/fluxcd/pkg/runtime/reconcile"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
- "github.com/fluxcd/image-reflector-controller/internal/secret"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
// latestTagsCount is the number of tags to use as latest tags.
@@ -112,7 +107,8 @@ type ImageRepositoryReconciler struct {
DatabaseWriter
DatabaseReader
}
- DeprecatedLoginOpts login.ProviderOptions
+
+ RegistryHelper registry.Helper
patchOptions []patch.Option
}
@@ -261,7 +257,7 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
}
conditions.Delete(obj, meta.StalledCondition)
- opts, err := r.setAuthOptions(ctx, obj, ref)
+ opts, err := r.RegistryHelper.GetAuthOptions(ctx, obj, ref)
if err != nil {
e := fmt.Errorf("failed to configure authentication options: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.AuthenticationFailedReason, e.Error())
@@ -327,107 +323,6 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
return
}
-// setAuthOptions returns authentication options required to scan a repository.
-func (r *ImageRepositoryReconciler) setAuthOptions(ctx context.Context, obj *imagev1.ImageRepository, ref name.Reference) ([]remote.Option, error) {
- timeout := obj.GetTimeout()
- ctx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
-
- // Configure authentication strategy to access the registry.
- var options []remote.Option
- var authSecret corev1.Secret
- var auth authn.Authenticator
- var authErr error
-
- if obj.Spec.SecretRef != nil {
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.SecretRef.Name,
- }, &authSecret); err != nil {
- return nil, err
- }
- auth, authErr = secret.AuthFromSecret(authSecret, ref)
- } else {
- // Build login provider options and use it to attempt registry login.
- opts := login.ProviderOptions{}
- switch obj.GetProvider() {
- case "aws":
- opts.AwsAutoLogin = true
- case "azure":
- opts.AzureAutoLogin = true
- case "gcp":
- opts.GcpAutoLogin = true
- default:
- opts = r.DeprecatedLoginOpts
- }
- auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
- }
- if authErr != nil {
- // If it's not unconfigured provider error, abort reconciliation.
- // Continue reconciliation if it's unconfigured providers for scanning
- // public repositories.
- if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
- return nil, authErr
- }
- }
- if auth != nil {
- options = append(options, remote.WithAuth(auth))
- }
-
- // Load any provided certificate.
- if obj.Spec.CertSecretRef != nil {
- var certSecret corev1.Secret
- if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
- certSecret = authSecret
- } else {
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.CertSecretRef.Name,
- }, &certSecret); err != nil {
- return nil, err
- }
- }
-
- tr, err := secret.TransportFromSecret(&certSecret)
- if err != nil {
- return nil, err
- }
- options = append(options, remote.WithTransport(tr))
- }
-
- if obj.Spec.ServiceAccountName != "" {
- serviceAccount := corev1.ServiceAccount{}
- // Lookup service account
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.ServiceAccountName,
- }, &serviceAccount); err != nil {
- return nil, err
- }
-
- if len(serviceAccount.ImagePullSecrets) > 0 {
- imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
- for i, ips := range serviceAccount.ImagePullSecrets {
- var saAuthSecret corev1.Secret
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: ips.Name,
- }, &saAuthSecret); err != nil {
- return nil, err
- }
- imagePullSecrets[i] = saAuthSecret
- }
- keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
- if err != nil {
- return nil, err
- }
- options = append(options, remote.WithAuthFromKeychain(keychain))
- }
- }
-
- return options, nil
-}
-
// shouldScan takes an image repo and the time now, and returns whether
// the repository should be scanned now, and how long to wait for the
// next scan. It also returns the reason for the scan.
diff --git a/internal/controllers/imagerepository_controller_test.go b/internal/controllers/imagerepository_controller_test.go
index 4c3087a5..32f32d9e 100644
--- a/internal/controllers/imagerepository_controller_test.go
+++ b/internal/controllers/imagerepository_controller_test.go
@@ -264,7 +264,7 @@ func TestImageRepositoryReconciler_setAuthOptions(t *testing.T) {
ref, err := name.ParseReference(obj.Spec.Image)
g.Expect(err).ToNot(HaveOccurred())
- _, err = r.setAuthOptions(ctx, obj, ref)
+ _, err = r.RegistryHelper.GetAuthOptions(ctx, obj, ref)
g.Expect(err != nil).To(Equal(tt.wantErr))
})
}
diff --git a/internal/registry/helper.go b/internal/registry/helper.go
new file mode 100644
index 00000000..7c05d90c
--- /dev/null
+++ b/internal/registry/helper.go
@@ -0,0 +1,29 @@
+package registry
+
+import (
+ "context"
+
+ imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
+ "github.com/fluxcd/pkg/oci/auth/login"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type Helper interface {
+ GetAuthOptions(ctx context.Context, obj *imagev1.ImageRepository, ref name.Reference) ([]remote.Option, error)
+}
+
+type DefaultHelper struct {
+ k8sClient client.Client
+ DeprecatedLoginOpts login.ProviderOptions
+}
+
+var _ Helper = DefaultHelper{}
+
+func NewDefaultHelper(c client.Client, deprecatedLoginOpts login.ProviderOptions) DefaultHelper {
+ return DefaultHelper{
+ k8sClient: c,
+ DeprecatedLoginOpts: deprecatedLoginOpts,
+ }
+}
diff --git a/internal/registry/options.go b/internal/registry/options.go
new file mode 100644
index 00000000..4ea9c905
--- /dev/null
+++ b/internal/registry/options.go
@@ -0,0 +1,118 @@
+package registry
+
+import (
+ "context"
+ "errors"
+
+ imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
+ "github.com/fluxcd/image-reflector-controller/internal/secret"
+ "github.com/fluxcd/pkg/oci"
+ "github.com/fluxcd/pkg/oci/auth/login"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/types"
+)
+
+// GetAuthOptions returns authentication options required to scan a repository.
+func (h DefaultHelper) GetAuthOptions(ctx context.Context, obj *imagev1.ImageRepository, ref name.Reference) ([]remote.Option, error) {
+ timeout := obj.GetTimeout()
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ // Configure authentication strategy to access the registry.
+ var options []remote.Option
+ var authSecret corev1.Secret
+ var auth authn.Authenticator
+ var authErr error
+
+ if obj.Spec.SecretRef != nil {
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.SecretRef.Name,
+ }, &authSecret); err != nil {
+ return nil, err
+ }
+ auth, authErr = secret.AuthFromSecret(authSecret, ref)
+ } else {
+ // Build login provider options and use it to attempt registry login.
+ opts := login.ProviderOptions{}
+ switch obj.GetProvider() {
+ case "aws":
+ opts.AwsAutoLogin = true
+ case "azure":
+ opts.AzureAutoLogin = true
+ case "gcp":
+ opts.GcpAutoLogin = true
+ default:
+ opts = h.DeprecatedLoginOpts
+ }
+ auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
+ }
+ if authErr != nil {
+ // If it's not unconfigured provider error, abort reconciliation.
+ // Continue reconciliation if it's unconfigured providers for scanning
+ // public repositories.
+ if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
+ return nil, authErr
+ }
+ }
+ if auth != nil {
+ options = append(options, remote.WithAuth(auth))
+ }
+
+ // Load any provided certificate.
+ if obj.Spec.CertSecretRef != nil {
+ var certSecret corev1.Secret
+ if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
+ certSecret = authSecret
+ } else {
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.CertSecretRef.Name,
+ }, &certSecret); err != nil {
+ return nil, err
+ }
+ }
+
+ tr, err := secret.TransportFromSecret(&certSecret)
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, remote.WithTransport(tr))
+ }
+
+ if obj.Spec.ServiceAccountName != "" {
+ serviceAccount := corev1.ServiceAccount{}
+ // Lookup service account
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.ServiceAccountName,
+ }, &serviceAccount); err != nil {
+ return nil, err
+ }
+
+ if len(serviceAccount.ImagePullSecrets) > 0 {
+ imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
+ for i, ips := range serviceAccount.ImagePullSecrets {
+ var saAuthSecret corev1.Secret
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: ips.Name,
+ }, &saAuthSecret); err != nil {
+ return nil, err
+ }
+ imagePullSecrets[i] = saAuthSecret
+ }
+ keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, remote.WithAuthFromKeychain(keychain))
+ }
+ }
+
+ return options, nil
+}
diff --git a/main.go b/main.go
index aae1c762..12239a03 100644
--- a/main.go
+++ b/main.go
@@ -49,6 +49,7 @@ import (
"github.com/fluxcd/image-reflector-controller/internal/controllers"
"github.com/fluxcd/image-reflector-controller/internal/database"
"github.com/fluxcd/image-reflector-controller/internal/features"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
const controllerName = "image-reflector-controller"
@@ -196,17 +197,20 @@ func main() {
metricsH := helper.MustMakeMetrics(mgr)
+ deprecatedLoginOptions := login.ProviderOptions{
+ AwsAutoLogin: awsAutoLogin,
+ AzureAutoLogin: azureAutoLogin,
+ GcpAutoLogin: gcpAutoLogin,
+ }
+ registryHelper := registry.NewDefaultHelper(mgr.GetClient(), deprecatedLoginOptions)
+
if err := (&controllers.ImageRepositoryReconciler{
Client: mgr.GetClient(),
EventRecorder: eventRecorder,
Metrics: metricsH,
Database: db,
ControllerName: controllerName,
- DeprecatedLoginOpts: login.ProviderOptions{
- AwsAutoLogin: awsAutoLogin,
- AzureAutoLogin: azureAutoLogin,
- GcpAutoLogin: gcpAutoLogin,
- },
+ RegistryHelper: registryHelper,
}).SetupWithManager(mgr, controllers.ImageRepositoryReconcilerOptions{
MaxConcurrentReconciles: concurrent,
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
@@ -221,6 +225,7 @@ func main() {
Database: db,
ACLOptions: aclOptions,
ControllerName: controllerName,
+ RegistryHelper: registryHelper,
}).SetupWithManager(mgr, controllers.ImagePolicyReconcilerOptions{
MaxConcurrentReconciles: concurrent,
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
|