Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
# controller-gen.kubebuilder.io/version: v0.18.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to hear your thinking behind this change

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This information is minimally interesting during development, but AFAIK has no use after that. As an annotation, it is deployed with the CRD but serves no purpose.

My impression has been that the generator is using metadata.annotations to push "stuff" through code that can only handle K8s objects.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
name: crunchybridgeclusters.postgres-operator.crunchydata.com
spec:
group: postgres-operator.crunchydata.com
Expand Down
20 changes: 13 additions & 7 deletions config/crd/bases/postgres-operator.crunchydata.com_pgadmins.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
# controller-gen.kubebuilder.io/version: v0.18.0
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
name: pgadmins.postgres-operator.crunchydata.com
spec:
group: postgres-operator.crunchydata.com
Expand Down Expand Up @@ -2621,7 +2620,9 @@ spec:
type: array
x-kubernetes-list-type: set
image:
description: Details for adding an image volume
description: |-
Reference to an image or OCI artifact.
More info: https://kubernetes.io/docs/concepts/storage/volumes#image
properties:
pullPolicy:
description: |-
Expand All @@ -2630,6 +2631,11 @@ spec:
Never: the kubelet never pulls the reference and only uses a local image or artifact. Container creation will fail if the reference isn't present.
IfNotPresent: the kubelet pulls if the reference isn't already present on disk. Container creation will fail if the reference isn't present and the pull fails.
Defaults to Always if :latest tag is specified, or IfNotPresent otherwise.
enum:
- Always
- Never
- IfNotPresent
maxLength: 12
type: string
reference:
description: |-
Expand All @@ -2639,7 +2645,10 @@ spec:
More info: https://kubernetes.io/docs/concepts/containers/images
This field is optional to allow higher level config management to default or override
container images in workload controllers like Deployments and StatefulSets.
minLength: 1
type: string
required:
- reference
type: object
name:
description: |-
Expand All @@ -2660,11 +2669,8 @@ spec:
x-kubernetes-validations:
- message: you must set only one of image or claimName
rule: has(self.claimName) != has(self.image)
- message: readOnly cannot be set false when using an ImageVolumeSource
- message: image volumes must be readOnly
rule: '!has(self.image) || !has(self.readOnly) || self.readOnly'
- message: if using an ImageVolumeSource, you must set a reference
rule: '!has(self.image) || (self.?image.reference.hasValue()
&& self.image.reference.size() > 0)'
maxItems: 10
type: array
x-kubernetes-list-map-keys:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
# controller-gen.kubebuilder.io/version: v0.18.0
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.3
name: pgupgrades.postgres-operator.crunchydata.com
spec:
group: postgres-operator.crunchydata.com
Expand Down
269 changes: 169 additions & 100 deletions config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.33.0 // indirect
k8s.io/apiserver v0.33.0 // indirect
k8s.io/code-generator v0.33.0 // indirect
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/controller-tools v0.17.3 // indirect
sigs.k8s.io/controller-tools v0.18.0 // indirect
Copy link
Member Author

@cbandy cbandy Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 v0.18 requires k8s.io/.. v0.33, but it wasn't bundled in the k8s.io updates last month: #4248

I don't know why!

sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
)
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,12 @@ k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw=
k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY=
k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4=
k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o=
k8s.io/component-base v0.33.4 h1:Jvb/aw/tl3pfgnJ0E0qPuYLT0NwdYs1VXXYQmSuxJGY=
k8s.io/component-base v0.33.4/go.mod h1:567TeSdixWW2Xb1yYUQ7qk5Docp2kNznKL87eygY8Rc=
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog=
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
Expand All @@ -366,8 +370,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUo
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw=
sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI=
sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9xrsE=
sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
Expand Down
7 changes: 6 additions & 1 deletion internal/crd/post-process.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log/slog"
"os"
"path/filepath"
"regexp"

"github.com/itchyny/gojq"
"sigs.k8s.io/yaml"
Expand Down Expand Up @@ -44,8 +45,12 @@ func main() {
panic(err)
}

// Turn top-level strings that start with octothorpe U+0023 into YAML comments by removing their quotes.
yamlData := need(yaml.Marshal(v))
yamlData = regexp.MustCompile(`(?m)^'(#[^']*)'(.*)$`).ReplaceAll(yamlData, []byte("$1$2"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we can't just capture the whole line as a single capture group because you want to exclude the quotes, yeah?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It doesn't seem like I need the second capture... The line should end just after the quoted string, right? 🦆

I'll check this tomorrow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we can't just capture the whole line as a single capture group because you want to exclude the quotes, yeah?

Right. This finds lines starting with '# and removes a pair of '. This is the transformation:

-'# controller-gen.kubebuilder.io/version': v0.18.0
+# controller-gen.kubebuilder.io/version: v0.18.0

🤔 It doesn't seem like I need the second capture...

I forgot that the quotes are around only the key of the flow mapping. The second capture is for the remainder of the line, if any.


slog.Info("Writing", "file", yamlName)
must(os.WriteFile(yamlPath, append([]byte("---\n"), need(yaml.Marshal(v))...), 0o644))
must(os.WriteFile(yamlPath, append([]byte("---\n"), yamlData...), 0o644))
}

if _, ok := result.Next(); ok {
Expand Down
31 changes: 31 additions & 0 deletions internal/crd/post-process.jq
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@
# SPDX-License-Identifier: Apache-2.0
#
# This [jq] filter modifies a Kubernetes CustomResourceDefinition.
# Use the controller-gen "+kubebuilder:title" marker to identify schemas that need special manipulation.
#
# [jq]: https://jqlang.org

# merge recursively combines a stream of objects.
# https://jqlang.org/manual#multiplication-division-modulo
def merge(stream): reduce stream as $i ({}; . * $i);

# https://pkg.go.dev/k8s.io/api/core/v1#ImageVolumeSource
reduce paths(try .title == "$corev1.ImageVolumeSource") as $path (.;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This may very well be "too clever." I'm curious what others think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting a title field on our CRD to add some validation rules and then removing the title field -- is that the part you're thinking might be too clever?

Seems reasonable to me, with the caveat that having the CRD be assembled from kubebuilder annotations and then post-process functions might make it a little harder to know easily where some element is coming from. Maybe that's a documentation issue, e.g., when we add the title in the struct, we could link to or reference this jq script at this point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting a title field on our CRD to add some validation rules and then removing the title field -- is that the part you're thinking might be too clever?

Yeah. Smuggling information through a marker that does something else.

Seems reasonable to me, with the caveat that having the CRD be assembled from kubebuilder annotations and then post-process functions might make it a little harder to know easily where some element is coming from. Maybe that's a documentation issue, e.g., when we add the title in the struct, we could link to or reference this jq script at this point?

A comment on the marker is better than none. I'll add one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment on the marker is better than none. I'll add one.

Done.

getpath($path) as $schema |
setpath($path; $schema * {
required: (["reference"] + ($schema.required // []) | sort),
properties: {
pullPolicy: { enum: ["Always", "Never", "IfNotPresent"] },
reference: { minLength: 1 }
}
} | del(.title))
) |

# Kubernetes assumes the evaluation cost of an enum value is very large: https://issue.k8s.io/119511
# Look at every schema that has a populated "enum" property.
reduce paths(try .enum | length > 0) as $path (.;
Expand Down Expand Up @@ -64,4 +77,22 @@ reduce paths(try .["x-kubernetes-int-or-string"] == true) as $path (.;
end
) |

# Rename Kubebuilder annotations and move them to the top-level.
# The caller can turn these into YAML comments.
. += (.metadata.annotations | with_entries(select(.key | startswith("controller-gen.kubebuilder")) | .key = "# \(.key)")) |
.metadata.annotations |= with_entries(select(.key | startswith("controller-gen.kubebuilder") | not)) |

# Remove nulls and empty objects from metadata.
# Some very old generators would set a null creationTimestamp.
#
# https://github.com/kubernetes-sigs/controller-tools/issues/402
# https://issue.k8s.io/67610
del(.metadata | .. | select(length == 0)) |

# Remove status to avoid conflicts with the CRD controller.
# Some very old generators would set this field.
#
# https://github.com/kubernetes-sigs/controller-tools/issues/456
del(.status) |

.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

"github.com/crunchydata/postgres-operator/internal/testing/cmp"
"github.com/crunchydata/postgres-operator/internal/testing/require"
v1 "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1"
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
Expand Down Expand Up @@ -110,6 +111,7 @@ func TestPostgresUserInterfaceAcrossVersions(t *testing.T) {
func TestAdditionalVolumes(t *testing.T) {
ctx := context.Background()
cc := require.KubernetesAtLeast(t, "1.30")
dryrun := client.NewDryRunClient(cc)
t.Parallel()

namespace := require.Namespace(t, cc)
Expand Down Expand Up @@ -154,8 +156,13 @@ func TestAdditionalVolumes(t *testing.T) {
}]
}
}]`, "spec", "instances")
err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)

err := dryrun.Create(ctx, tmp.DeepCopy())
assert.Assert(t, apierrors.IsInvalid(err))

details := require.StatusErrorDetails(t, err)
assert.Assert(t, cmp.Len(details.Causes, 1))
assert.Equal(t, details.Causes[0].Field, "spec.instances[0].volumes.additional[0]")
assert.ErrorContains(t, err, "you must set only one of image or claimName")
})

Expand All @@ -178,9 +185,14 @@ func TestAdditionalVolumes(t *testing.T) {
}]
}
}]`, "spec", "instances")
err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)

err := dryrun.Create(ctx, tmp.DeepCopy())
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "readOnly cannot be set false when using an ImageVolumeSource")

details := require.StatusErrorDetails(t, err)
assert.Assert(t, cmp.Len(details.Causes, 1))
assert.Equal(t, details.Causes[0].Field, "spec.instances[0].volumes.additional[0]")
assert.ErrorContains(t, err, "image volumes must be readOnly")
})

t.Run("Reference must be set when using image volume", func(t *testing.T) {
Expand All @@ -201,9 +213,15 @@ func TestAdditionalVolumes(t *testing.T) {
}]
}
}]`, "spec", "instances")
err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)

err := dryrun.Create(ctx, tmp.DeepCopy())
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "if using an ImageVolumeSource, you must set a reference")

details := require.StatusErrorDetails(t, err)
assert.Assert(t, cmp.Len(details.Causes, 2))
assert.Assert(t, cmp.Equal(details.Causes[0].Field, "spec.instances[0].volumes.additional[0].image.reference"))
assert.Assert(t, cmp.Equal(details.Causes[0].Type, "FieldValueRequired"))
assert.ErrorContains(t, err, "Required")
})

t.Run("Reference cannot be an empty string when using image volume", func(t *testing.T) {
Expand All @@ -225,9 +243,15 @@ func TestAdditionalVolumes(t *testing.T) {
}]
}
}]`, "spec", "instances")
err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)

err := dryrun.Create(ctx, tmp.DeepCopy())
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "if using an ImageVolumeSource, you must set a reference")

details := require.StatusErrorDetails(t, err)
assert.Assert(t, cmp.Len(details.Causes, 1))
assert.Assert(t, cmp.Equal(details.Causes[0].Field, "spec.instances[0].volumes.additional[0].image.reference"))
assert.Assert(t, cmp.Equal(details.Causes[0].Type, "FieldValueInvalid"))
assert.ErrorContains(t, err, "at least 1 chars long")
})

t.Run("ReadOnly can be omitted or set true when using image volume", func(t *testing.T) {
Expand Down Expand Up @@ -265,6 +289,6 @@ func TestAdditionalVolumes(t *testing.T) {
}]
}
}]`, "spec", "instances")
assert.NilError(t, cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll))
assert.NilError(t, dryrun.Create(ctx, tmp.DeepCopy()))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type PostgresClusterSpec struct {
// e.g. RELATED_IMAGE_POSTGRES_13. For PostGIS enabled PostgreSQL images,
// the format is RELATED_IMAGE_POSTGRES_{postgresVersion}_GIS_{postGISVersion},
// e.g. RELATED_IMAGE_POSTGRES_13_GIS_3.1.
// ---
// [corev1.Container.Image]
//
// +optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,order=1
Image string `json:"image,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,7 @@ func (meta *Metadata) GetAnnotationsOrNil() map[string]string {
// +structType=atomic
//
// +kubebuilder:validation:XValidation:rule=`has(self.claimName) != has(self.image)`,message=`you must set only one of image or claimName`
// +kubebuilder:validation:XValidation:rule=`!has(self.image) || !has(self.readOnly) || self.readOnly`,message=`readOnly cannot be set false when using an ImageVolumeSource`
// +kubebuilder:validation:XValidation:rule=`!has(self.image) || (self.?image.reference.hasValue() && self.image.reference.size() > 0)`,message=`if using an ImageVolumeSource, you must set a reference`
// +kubebuilder:validation:XValidation:rule=`!has(self.image) || !has(self.readOnly) || self.readOnly`,message=`image volumes must be readOnly`
type AdditionalVolume struct {
// Name of an existing PersistentVolumeClaim.
// ---
Expand All @@ -337,9 +336,11 @@ type AdditionalVolume struct {
// +optional
Containers []DNS1123Label `json:"containers"`

// Details for adding an image volume
// Reference to an image or OCI artifact.
// More info: https://kubernetes.io/docs/concepts/storage/volumes#image
// ---
// https://docs.k8s.io/concepts/storage/volumes#image
// Use "title" to add more validation in [internal/crd/post-process.jq].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// +kubebuilder:title=$corev1.ImageVolumeSource
//
// +optional
Image *corev1.ImageVolumeSource `json:"image,omitempty"`
Expand Down
Loading