Skip to content

Commit 75ced26

Browse files
committed
feat: basic image mirroring (no-auth, amd64 only)
1 parent 9a1dddc commit 75ced26

File tree

7 files changed

+113
-138
lines changed

7 files changed

+113
-138
lines changed

api/kuik/v1alpha1/imagemirror_types.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ type ImageMirrorSpec struct {
1515

1616
// ImageMirrorStatus defines the observed state of ImageMirror.
1717
type ImageMirrorStatus struct {
18-
// Digest is the digest of the mirrored image
19-
Digest string `json:"digest,omitempty"`
20-
Conditions []metav1.Condition `json:"conditions,omitempty"`
18+
// Phase represents the current phase of the ImageMirror
19+
Phase string `json:"phase,omitempty"`
2120
}
2221

2322
// +kubebuilder:object:root=true
@@ -26,6 +25,7 @@ type ImageMirrorStatus struct {
2625
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.path"
2726
// +kubebuilder:printcolumn:name="From",type="string",JSONPath=".spec.registry"
2827
// +kubebuilder:printcolumn:name="To",type="string",JSONPath=".spec.targetRegistry"
28+
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase"
2929
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
3030

3131
// ImageMirror is the Schema for the imagemirrors API.
@@ -49,3 +49,19 @@ type ImageMirrorList struct {
4949
func init() {
5050
SchemeBuilder.Register(&ImageMirror{}, &ImageMirrorList{})
5151
}
52+
53+
func (i *ImageMirrorSpec) TargetReference() string {
54+
if i.TargetRegistry == "" {
55+
return i.Path
56+
}
57+
58+
return i.TargetRegistry + "/" + i.Path
59+
}
60+
61+
func (i *ImageMirror) SourceReference() string {
62+
return i.Spec.Reference()
63+
}
64+
65+
func (i *ImageMirror) TargetReference() string {
66+
return i.Spec.TargetReference()
67+
}

api/kuik/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/kuik.enix.io_imagemirrors.yaml

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ spec:
2626
- jsonPath: .spec.targetRegistry
2727
name: To
2828
type: string
29+
- jsonPath: .status.phase
30+
name: Status
31+
type: string
2932
- jsonPath: .metadata.creationTimestamp
3033
name: Age
3134
type: date
@@ -72,64 +75,8 @@ spec:
7275
status:
7376
description: ImageMirrorStatus defines the observed state of ImageMirror.
7477
properties:
75-
conditions:
76-
items:
77-
description: Condition contains details for one aspect of the current
78-
state of this API Resource.
79-
properties:
80-
lastTransitionTime:
81-
description: |-
82-
lastTransitionTime is the last time the condition transitioned from one status to another.
83-
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
84-
format: date-time
85-
type: string
86-
message:
87-
description: |-
88-
message is a human readable message indicating details about the transition.
89-
This may be an empty string.
90-
maxLength: 32768
91-
type: string
92-
observedGeneration:
93-
description: |-
94-
observedGeneration represents the .metadata.generation that the condition was set based upon.
95-
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
96-
with respect to the current state of the instance.
97-
format: int64
98-
minimum: 0
99-
type: integer
100-
reason:
101-
description: |-
102-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
103-
Producers of specific condition types may define expected values and meanings for this field,
104-
and whether the values are considered a guaranteed API.
105-
The value should be a CamelCase string.
106-
This field may not be empty.
107-
maxLength: 1024
108-
minLength: 1
109-
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
110-
type: string
111-
status:
112-
description: status of the condition, one of True, False, Unknown.
113-
enum:
114-
- "True"
115-
- "False"
116-
- Unknown
117-
type: string
118-
type:
119-
description: type of condition in CamelCase or in foo.example.com/CamelCase.
120-
maxLength: 316
121-
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
122-
type: string
123-
required:
124-
- lastTransitionTime
125-
- message
126-
- reason
127-
- status
128-
- type
129-
type: object
130-
type: array
131-
digest:
132-
description: Digest is the digest of the mirrored image
78+
phase:
79+
description: Phase represents the current phase of the ImageMirror
13380
type: string
13481
type: object
13582
type: object

helm/kube-image-keeper/crds/kuik.enix.io_imagemirrors.yaml

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ spec:
2525
- jsonPath: .spec.targetRegistry
2626
name: To
2727
type: string
28+
- jsonPath: .status.phase
29+
name: Status
30+
type: string
2831
- jsonPath: .metadata.creationTimestamp
2932
name: Age
3033
type: date
@@ -71,64 +74,8 @@ spec:
7174
status:
7275
description: ImageMirrorStatus defines the observed state of ImageMirror.
7376
properties:
74-
conditions:
75-
items:
76-
description: Condition contains details for one aspect of the current
77-
state of this API Resource.
78-
properties:
79-
lastTransitionTime:
80-
description: |-
81-
lastTransitionTime is the last time the condition transitioned from one status to another.
82-
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
83-
format: date-time
84-
type: string
85-
message:
86-
description: |-
87-
message is a human readable message indicating details about the transition.
88-
This may be an empty string.
89-
maxLength: 32768
90-
type: string
91-
observedGeneration:
92-
description: |-
93-
observedGeneration represents the .metadata.generation that the condition was set based upon.
94-
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
95-
with respect to the current state of the instance.
96-
format: int64
97-
minimum: 0
98-
type: integer
99-
reason:
100-
description: |-
101-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
102-
Producers of specific condition types may define expected values and meanings for this field,
103-
and whether the values are considered a guaranteed API.
104-
The value should be a CamelCase string.
105-
This field may not be empty.
106-
maxLength: 1024
107-
minLength: 1
108-
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
109-
type: string
110-
status:
111-
description: status of the condition, one of True, False, Unknown.
112-
enum:
113-
- "True"
114-
- "False"
115-
- Unknown
116-
type: string
117-
type:
118-
description: type of condition in CamelCase or in foo.example.com/CamelCase.
119-
maxLength: 316
120-
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
121-
type: string
122-
required:
123-
- lastTransitionTime
124-
- message
125-
- reason
126-
- status
127-
- type
128-
type: object
129-
type: array
130-
digest:
131-
description: Digest is the digest of the mirrored image
77+
phase:
78+
description: Phase represents the current phase of the ImageMirror
13279
type: string
13380
type: object
13481
type: object

internal/controller/kuik/image_controller.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"k8s.io/apimachinery/pkg/runtime"
1717
"k8s.io/apimachinery/pkg/types"
1818
ctrl "sigs.k8s.io/controller-runtime"
19+
"sigs.k8s.io/controller-runtime/pkg/builder"
1920
"sigs.k8s.io/controller-runtime/pkg/client"
2021
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2122
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -77,21 +78,19 @@ func (r *ImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
7778
registries := routing.MatchingRegistries(r.Config, &image.Spec.ImageReference)
7879
for _, reg := range registries {
7980
if reg.MirroringEnabled {
80-
name, err := registry.ImageNameFromReference(reg.Url + "/" + image.Spec.Path)
81-
if err != nil {
82-
return ctrl.Result{}, err
83-
}
84-
8581
imageMirror := &kuikv1alpha1.ImageMirror{
86-
ObjectMeta: v1.ObjectMeta{
87-
Name: name,
88-
},
8982
Spec: kuikv1alpha1.ImageMirrorSpec{
9083
ImageReference: image.Spec.ImageReference,
9184
TargetRegistry: reg.Url,
9285
},
9386
}
9487

88+
name, err := registry.ImageNameFromReference(imageMirror.TargetReference())
89+
if err != nil {
90+
return ctrl.Result{}, err
91+
}
92+
imageMirror.Name = name
93+
9594
op, err := controllerutil.CreateOrPatch(ctx, r.Client, imageMirror, func() error {
9695
if err := controllerutil.SetOwnerReference(&image, imageMirror, r.Scheme); err != nil {
9796
return err
@@ -135,6 +134,7 @@ func (r *ImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
135134
&corev1.Pod{},
136135
handler.EnqueueRequestsFromMapFunc(r.imagesRequestFromPod),
137136
).
137+
Owns(&kuikv1alpha1.ImageMirror{}, builder.MatchEveryOwner).
138138
Complete(r)
139139
}
140140

internal/controller/kuik/imagemirror_controller.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package kuik
22

33
import (
44
"context"
5+
"net/http"
6+
"time"
57

68
"k8s.io/apimachinery/pkg/runtime"
79
ctrl "sigs.k8s.io/controller-runtime"
810
"sigs.k8s.io/controller-runtime/pkg/client"
911
logf "sigs.k8s.io/controller-runtime/pkg/log"
12+
"sigs.k8s.io/controller-runtime/pkg/predicate"
1013

1114
kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1"
15+
"github.com/enix/kube-image-keeper/internal/registry"
1216
)
1317

1418
// ImageMirrorReconciler reconciles a ImageMirror object
@@ -31,17 +35,79 @@ type ImageMirrorReconciler struct {
3135
// For more details, check Reconcile and its Result here:
3236
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
3337
func (r *ImageMirrorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
34-
_ = logf.FromContext(ctx)
38+
log := logf.FromContext(ctx)
3539

36-
// TODO(user): your logic here
40+
var imageMirror kuikv1alpha1.ImageMirror
41+
if err := r.Get(ctx, req.NamespacedName, &imageMirror); err != nil {
42+
return ctrl.Result{}, client.IgnoreNotFound(err)
43+
}
44+
45+
if !imageMirror.DeletionTimestamp.IsZero() {
46+
return ctrl.Result{}, nil
47+
}
48+
49+
available := r.isImageAvailableOnTarget(logf.IntoContext(ctx, log), imageMirror)
50+
51+
log.Info("checked availability", "available", available)
52+
53+
if !available {
54+
if imageMirror.Status.Phase != "Mirroring" {
55+
patch := client.MergeFrom(imageMirror.DeepCopy())
56+
imageMirror.Status.Phase = "Mirroring"
57+
if err := r.Status().Patch(ctx, &imageMirror, patch); err != nil {
58+
return ctrl.Result{}, err
59+
}
60+
}
61+
62+
err := r.mirrorImage(logf.IntoContext(ctx, log), imageMirror)
63+
if err != nil {
64+
patch := client.MergeFrom(imageMirror.DeepCopy())
65+
if imageMirror.Status.Phase != "Error" {
66+
imageMirror.Status.Phase = "Error"
67+
if err := r.Status().Patch(ctx, &imageMirror, patch); err != nil {
68+
return ctrl.Result{}, err
69+
}
70+
}
71+
return ctrl.Result{}, err
72+
}
73+
74+
log.Info("image successfully mirrored")
75+
}
76+
77+
if imageMirror.Status.Phase != "Ready" {
78+
patch := client.MergeFrom(imageMirror.DeepCopy())
79+
imageMirror.Status.Phase = "Ready"
80+
if err := r.Status().Patch(ctx, &imageMirror, patch); err != nil {
81+
return ctrl.Result{}, err
82+
}
83+
}
3784

3885
return ctrl.Result{}, nil
3986
}
4087

88+
func (r *ImageMirrorReconciler) isImageAvailableOnTarget(ctx context.Context, imageMirror kuikv1alpha1.ImageMirror) bool {
89+
log := logf.FromContext(ctx)
90+
log.V(1).Info("verifying image availability on target registry", "reference", imageMirror.Spec.ImageReference)
91+
_, err := registry.NewClient(nil, nil).WithPullSecrets(nil).ReadDescriptor(http.MethodGet, imageMirror.TargetReference(), time.Second*30)
92+
return err == nil
93+
}
94+
95+
func (r *ImageMirrorReconciler) mirrorImage(ctx context.Context, imageMirror kuikv1alpha1.ImageMirror) error {
96+
log := logf.FromContext(ctx)
97+
log.Info("mirroring image", "reference", imageMirror.Spec.ImageReference)
98+
return registry.MirrorImage(imageMirror.SourceReference(), imageMirror.TargetReference())
99+
}
100+
41101
// SetupWithManager sets up the controller with the Manager.
42102
func (r *ImageMirrorReconciler) SetupWithManager(mgr ctrl.Manager) error {
43103
return ctrl.NewControllerManagedBy(mgr).
44104
For(&kuikv1alpha1.ImageMirror{}).
45105
Named("kuik-imagemirror").
106+
// prevent from reenquing after status update (produced a infinite loop between Error and Mirroring phases)
107+
WithEventFilter(predicate.Or(
108+
predicate.GenerationChangedPredicate{},
109+
predicate.LabelChangedPredicate{},
110+
predicate.AnnotationChangedPredicate{},
111+
)).
46112
Complete(r)
47113
}

internal/registry/registry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/cespare/xxhash/v2"
1414
"github.com/distribution/reference"
15+
"github.com/google/go-containerregistry/pkg/crane"
1516
"github.com/google/go-containerregistry/pkg/name"
1617
v1 "github.com/google/go-containerregistry/pkg/v1"
1718
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -83,6 +84,12 @@ func (a *AuthenticatedClient) ReadDescriptor(httpMethod string, imageName string
8384
return nil, returnedErr
8485
}
8586

87+
func MirrorImage(from, to string) error {
88+
return crane.Copy(from, to, func(o *crane.Options) {
89+
o.Platform, _ = v1.ParsePlatform("amd64")
90+
})
91+
}
92+
8693
func ImageNameFromReference(image string) (string, error) {
8794
ref, err := reference.ParseAnyReference(image)
8895
if err != nil {

0 commit comments

Comments
 (0)