From e807e03478cdbb8bc687d636bc4d6eeb926256b2 Mon Sep 17 00:00:00 2001 From: reoring Date: Wed, 21 Oct 2026 14:52:05 +0900 Subject: [PATCH] hosted: admit the full uint32 serial range through the apiserver (format int64) controller-gen emits format int32 for uint32 fields, and the apiserver's format-aware bound check rejects a maximum of 4294967295 against int32 ("Maximum boundary value must be of type integer with format int32"), so every HostedZone create failed live (SnapshotRejected). Override the schema format on spec.serial and status.servers[].serial and pin the range with a real-apiserver envtest. --- .../hosted.dns.appthrust.io_hostedzones.yaml | 4 +- .../hosted.dns.appthrust.io_hostedzones.yaml | 4 +- pkg/go/api/hosted/v1alpha1/envtest_test.go | 84 +++++++++++++++++++ pkg/go/api/hosted/v1alpha1/types.go | 2 + 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 pkg/go/api/hosted/v1alpha1/envtest_test.go diff --git a/app/operator/config/crd/bases/hosted.dns.appthrust.io_hostedzones.yaml b/app/operator/config/crd/bases/hosted.dns.appthrust.io_hostedzones.yaml index 7ebcdec..63515bf 100644 --- a/app/operator/config/crd/bases/hosted.dns.appthrust.io_hostedzones.yaml +++ b/app/operator/config/crd/bases/hosted.dns.appthrust.io_hostedzones.yaml @@ -136,7 +136,7 @@ spec: x-kubernetes-list-type: map serial: description: Serial increases on each content change. - format: int32 + format: int64 maximum: 4294967295 minimum: 1 type: integer @@ -212,7 +212,7 @@ spec: format: date-time type: string serial: - format: int32 + format: int64 maximum: 4294967295 minimum: 1 type: integer diff --git a/deploy/charts/dns-api/crds/hosted.dns.appthrust.io_hostedzones.yaml b/deploy/charts/dns-api/crds/hosted.dns.appthrust.io_hostedzones.yaml index 7ebcdec..63515bf 100644 --- a/deploy/charts/dns-api/crds/hosted.dns.appthrust.io_hostedzones.yaml +++ b/deploy/charts/dns-api/crds/hosted.dns.appthrust.io_hostedzones.yaml @@ -136,7 +136,7 @@ spec: x-kubernetes-list-type: map serial: description: Serial increases on each content change. - format: int32 + format: int64 maximum: 4294967295 minimum: 1 type: integer @@ -212,7 +212,7 @@ spec: format: date-time type: string serial: - format: int32 + format: int64 maximum: 4294967295 minimum: 1 type: integer diff --git a/pkg/go/api/hosted/v1alpha1/envtest_test.go b/pkg/go/api/hosted/v1alpha1/envtest_test.go new file mode 100644 index 0000000..3f6bd10 --- /dev/null +++ b/pkg/go/api/hosted/v1alpha1/envtest_test.go @@ -0,0 +1,84 @@ +package v1alpha1 + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +// The in-process schema validator does not apply the apiserver's +// format-aware bound check; a uint32 serial generated with format int32 and +// maximum 4294967295 was rejected live ("Maximum boundary value must be of +// type integer with format int32"). Admit the full serial range through a +// real apiserver. +func TestHostedZoneSerialRangeAdmittedByAPIServer(t *testing.T) { + assets := os.Getenv("KUBEBUILDER_ASSETS") + if assets == "" { + t.Skip("KUBEBUILDER_ASSETS is required for the real CRD admission test") + } + environment := &envtest.Environment{ + BinaryAssetsDirectory: assets, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "..", "app", "operator", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + ControlPlaneStartTimeout: 30 * time.Second, + ControlPlaneStopTimeout: 15 * time.Second, + } + t.Cleanup(func() { + if err := environment.Stop(); err != nil { + t.Errorf("stop envtest: %v", err) + } + }) + cfg, err := environment.Start() + if err != nil { + t.Fatal(err) + } + s := runtime.NewScheme() + if err := scheme.AddToScheme(s); err != nil { + t.Fatal(err) + } + if err := AddToScheme(s); err != nil { + t.Fatal(err) + } + c, err := client.New(cfg, client.Options{Scheme: s}) + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + if err := c.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "dns"}}); err != nil { + t.Fatal(err) + } + digest := "sha256:" + strings.Repeat("a", 64) + for _, serial := range []uint32{1, 2147483648, ^uint32(0)} { + zone := &HostedZone{ + ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "dns"}, + Spec: HostedZoneSpec{ + DomainName: "example.test", + Serial: serial, + SOA: SOA{MName: "ns.example.test", RName: "hostmaster.example.test", Refresh: 3600, Retry: 900, Expire: 1209600, Minimum: 300}, + NameServers: []NameServer{{Name: "ns.example.test", Addresses: []string{"192.0.2.53"}}}, + Records: []HostedZoneRecord{{Name: "@", Type: "A", TTL: 60, Values: []string{"192.0.2.1"}}}, + ContentDigest: digest, + }, + } + if err := c.Create(ctx, zone); err != nil { + t.Fatalf("serial %d rejected: %v", serial, err) + } + zone.Status.Servers = []HostedZoneServerStatus{{Endpoint: "dns/server", Serial: serial, ContentDigest: digest, ObservedAt: metav1.Now()}} + if err := c.Status().Update(ctx, zone); err != nil { + t.Fatalf("status serial %d rejected: %v", serial, err) + } + if err := c.Delete(ctx, zone); err != nil { + t.Fatal(err) + } + } +} diff --git a/pkg/go/api/hosted/v1alpha1/types.go b/pkg/go/api/hosted/v1alpha1/types.go index 8127d29..e4bb222 100644 --- a/pkg/go/api/hosted/v1alpha1/types.go +++ b/pkg/go/api/hosted/v1alpha1/types.go @@ -105,6 +105,7 @@ type HostedZoneSpec struct { // Serial increases on each content change. // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=4294967295 + // +kubebuilder:validation:Format=int64 Serial uint32 `json:"serial"` SOA SOA `json:"soa"` @@ -185,6 +186,7 @@ type HostedZoneServerStatus struct { Endpoint string `json:"endpoint"` // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=4294967295 + // +kubebuilder:validation:Format=int64 Serial uint32 `json:"serial"` // +kubebuilder:validation:Pattern=`^sha256:[0-9a-f]{64}$` ContentDigest string `json:"contentDigest"`