Skip to content

Commit 90a945e

Browse files
stubbiclaude
andauthored
fix: require explicit image tag or digest instead of defaulting to :latest (#54)
Drops the `+kubebuilder:default="latest"` from Instance.spec.image.tag and adds a CEL validation rule requiring at least one of `tag` or `digest` to be set. Removes the matching Go fallbacks in the StatefulSet builder and the auto-update reconciler. Fresh installs that relied on the default now fail admission with a clear message instead of silently pulling `ghcr.io/paperclipinc/paperclip:latest`, which ships a broken export map and crashes on startup with ERR_MODULE_NOT_FOUND. Samples and the envtest suite are updated to pin an explicit tag. Fixes #52 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95c41ec commit 90a945e

8 files changed

Lines changed: 39 additions & 15 deletions

File tree

api/v1alpha1/paperclipinstance_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,16 @@ type InstanceSpec struct {
135135
}
136136

137137
// ImageSpec configures the container image.
138+
// +kubebuilder:validation:XValidation:rule="size(self.tag) > 0 || size(self.digest) > 0",message="spec.image: one of tag or digest must be set (pinning to :latest is not supported, pick a specific upstream release tag)"
138139
type ImageSpec struct {
139140
// Repository is the container image repository.
140141
// +kubebuilder:default="ghcr.io/paperclipinc/paperclip"
141142
// +optional
142143
Repository string `json:"repository,omitempty"`
143144

144-
// Tag is the container image tag.
145-
// +kubebuilder:default="latest"
145+
// Tag is the container image tag. Either tag or digest must be set; there is
146+
// no default, because pinning to a mutable tag like :latest can silently pull
147+
// a broken upstream build.
146148
// +optional
147149
Tag string `json:"tag,omitempty"`
148150

charts/paperclip-operator/templates/crds/paperclip.inc_instances.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3998,10 +3998,17 @@ spec:
39983998
description: Repository is the container image repository.
39993999
type: string
40004000
tag:
4001-
default: latest
4002-
description: Tag is the container image tag.
4001+
description: |-
4002+
Tag is the container image tag. Either tag or digest must be set; there is
4003+
no default, because pinning to a mutable tag like :latest can silently pull
4004+
a broken upstream build.
40034005
type: string
40044006
type: object
4007+
x-kubernetes-validations:
4008+
- message: 'spec.image: one of tag or digest must be set (pinning
4009+
to :latest is not supported, pick a specific upstream release
4010+
tag)'
4011+
rule: size(self.tag) > 0 || size(self.digest) > 0
40054012
initContainers:
40064013
description: InitContainers specifies additional init containers.
40074014
items:

config/crd/bases/paperclip.inc_instances.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3992,10 +3992,17 @@ spec:
39923992
description: Repository is the container image repository.
39933993
type: string
39943994
tag:
3995-
default: latest
3996-
description: Tag is the container image tag.
3995+
description: |-
3996+
Tag is the container image tag. Either tag or digest must be set; there is
3997+
no default, because pinning to a mutable tag like :latest can silently pull
3998+
a broken upstream build.
39973999
type: string
39984000
type: object
4001+
x-kubernetes-validations:
4002+
- message: 'spec.image: one of tag or digest must be set (pinning
4003+
to :latest is not supported, pick a specific upstream release
4004+
tag)'
4005+
rule: size(self.tag) > 0 || size(self.digest) > 0
39994006
initContainers:
40004007
description: InitContainers specifies additional init containers.
40014008
items:

config/samples/paperclip_v1alpha1_instance.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ metadata:
66
spec:
77
image:
88
repository: ghcr.io/paperclipinc/paperclip
9-
tag: latest
9+
# Pick a specific upstream release tag. The operator rejects Instances
10+
# without a tag (or digest), since :latest can silently pull a broken build.
11+
# See the tags list at https://github.com/paperclipinc/paperclip/pkgs/container/paperclip
12+
tag: "2026.0403"
1013

1114
deployment:
1215
mode: authenticated

config/samples/paperclip_v1alpha1_instance_public.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ metadata:
66
spec:
77
image:
88
repository: ghcr.io/paperclipinc/paperclip
9-
tag: latest
9+
# Pick a specific upstream release tag. The operator rejects Instances
10+
# without a tag (or digest), since :latest can silently pull a broken build.
11+
# See the tags list at https://github.com/paperclipinc/paperclip/pkgs/container/paperclip
12+
tag: "2026.0403"
1013

1114
deployment:
1215
mode: authenticated

internal/controller/instance_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,9 @@ func (r *InstanceReconciler) reconcileAutoUpdate(ctx context.Context, instance *
11051105
}
11061106
tag := instance.Spec.Image.Tag
11071107
if tag == "" {
1108-
tag = "latest"
1108+
instance.Status.AutoUpdate.LastError = "auto-update requires spec.image.tag to be set; cannot poll a digest-pinned image"
1109+
instance.Status.AutoUpdate.LastCheckTime = &now
1110+
return ctrl.Result{RequeueAfter: interval}
11091111
}
11101112

11111113
digest, err := r.RegistryClient.ResolveDigest(ctx, repo, tag, dockerConfigJSON)

internal/controller/instance_controller_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ var _ = Describe("Instance Controller", func() {
5151
Name: resourceName,
5252
Namespace: "default",
5353
},
54-
// TODO(user): Specify other spec details if needed.
54+
Spec: paperclipv1alpha1.InstanceSpec{
55+
Image: paperclipv1alpha1.ImageSpec{
56+
Tag: "v1.0.0",
57+
},
58+
},
5559
}
5660
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
5761
}

internal/resources/statefulset.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -849,11 +849,7 @@ func containerImage(instance *paperclipv1alpha1.Instance) string {
849849
return repo + "@" + instance.Spec.Image.Digest
850850
}
851851

852-
tag := instance.Spec.Image.Tag
853-
if tag == "" {
854-
tag = "latest"
855-
}
856-
return repo + ":" + tag
852+
return repo + ":" + instance.Spec.Image.Tag
857853
}
858854

859855
func imagePullPolicy(instance *paperclipv1alpha1.Instance) corev1.PullPolicy {

0 commit comments

Comments
 (0)