From 54f39c67e876caef70df40f64ba24b0ed5bfb3f6 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:09:11 -0500 Subject: [PATCH 1/4] feat: Define internal DNS fleet delivery contracts --- api/v1alpha1/internaldns_binding_test.go | 37 + api/v1alpha1/internaldns_service_types.go | 264 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 672 ++++++++++++++++++ ...ing.miloapis.com_dnspublicationchunks.yaml | 74 ++ ....miloapis.com_dnspublicationmanifests.yaml | 293 ++++++++ ...miloapis.com_dnspublicationownerships.yaml | 75 ++ ...king.miloapis.com_dnsresolverbindings.yaml | 378 ++++++++++ ...ing.miloapis.com_dnstransportoutboxes.yaml | 139 ++++ config/crd/kustomization.yaml | 5 + go.mod | 5 +- go.sum | 52 +- internal/internaldns/transport/jetstream.go | 434 +++++++++++ .../internaldns/transport/jetstream_test.go | 55 ++ 13 files changed, 2440 insertions(+), 43 deletions(-) create mode 100644 api/v1alpha1/internaldns_binding_test.go create mode 100644 api/v1alpha1/internaldns_service_types.go create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml create mode 100644 internal/internaldns/transport/jetstream.go create mode 100644 internal/internaldns/transport/jetstream_test.go diff --git a/api/v1alpha1/internaldns_binding_test.go b/api/v1alpha1/internaldns_binding_test.go new file mode 100644 index 00000000..ce16887b --- /dev/null +++ b/api/v1alpha1/internaldns_binding_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "encoding/json" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestResolverBindingUsesNestedContractAndIndependentCopies(t *testing.T) { + original := &DNSResolverBinding{Spec: DNSResolverBindingSpec{ + Source: DNSResolverBindingSource{ProjectUID: "project", ResolverContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, AccessBindingRef: DNSObjectReference{Name: "access", UID: "access-uid"}}, + Placement: DNSResolverBindingPlacement{Region: "central", Shard: "shared"}, + Configuration: DNSResolverBindingConfiguration{Generation: 1, Revision: 2, Listeners: DNSResolverBindingListeners{Node: DNSResolverBindingListener{Address: "fd53::1", Port: 53, Transports: []string{"UDP", "TCP"}}, Regional: DNSResolverBindingListener{Address: "fd54::1", Port: 53, Transports: []string{"UDP", "TCP"}}}, ZoneRefs: []DNSObjectReference{{Name: "private", UID: "zone-uid"}}}, + Authorization: DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 4, ValidUntil: metav1.Now()}, + }} + copy := original.DeepCopy() + copy.Spec.Configuration.ZoneRefs[0].Name = "changed" + copy.Spec.Configuration.Listeners.Node.Transports[0] = "changed" + copy.Spec.Configuration.Listeners.Regional.Transports[0] = "changed" + if original.Spec.Configuration.ZoneRefs[0].Name != "private" || original.Spec.Configuration.Listeners.Node.Transports[0] != "UDP" || original.Spec.Configuration.Listeners.Regional.Transports[0] != "UDP" { + t.Fatal("deep copy aliases nested API collections") + } + raw, err := json.Marshal(original.Spec) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err = json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + if len(fields) != 4 || fields["source"] == nil || fields["placement"] == nil || fields["configuration"] == nil || fields["authorization"] == nil { + t.Fatalf("unexpected binding wire structure: %s", raw) + } +} diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go new file mode 100644 index 00000000..b55071d0 --- /dev/null +++ b/api/v1alpha1/internaldns_service_types.go @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// DNSResolverBinding is the DNS-service projection of a consumer context and access binding. +// +kubebuilder:validation:XValidation:rule="self.configuration.generation >= oldSelf.configuration.generation && self.configuration.revision >= oldSelf.configuration.revision",message="configuration fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence >= oldSelf.authorization.sequence)",message="authorization fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch != oldSelf.authorization.writerEpoch || self.authorization.sequence != oldSelf.authorization.sequence || self.authorization.validUntil == oldSelf.authorization.validUntil",message="changing an authorization deadline requires a new fence" +// +kubebuilder:validation:XValidation:rule="self.configuration.zoneRefs == oldSelf.configuration.zoneRefs || (self.configuration.generation > oldSelf.configuration.generation && self.configuration.revision > oldSelf.configuration.revision)",message="zone membership changes require a new cache generation and configuration revision" +// +kubebuilder:validation:XValidation:rule="(has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision",message="changing withdrawal state requires a new configuration revision" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence > oldSelf.authorization.sequence)",message="reactivating withdrawn access requires a new source authorization fence" +type DNSResolverBindingSpec struct { + Source DNSResolverBindingSource `json:"source"` + Placement DNSResolverBindingPlacement `json:"placement"` + Configuration DNSResolverBindingConfiguration `json:"configuration"` + Authorization DNSResolverAccessAuthorization `json:"authorization"` + Tombstone bool `json:"tombstone,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding source identity is immutable" +// +kubebuilder:validation:XValidation:rule="self.projectUID.size() > 0 && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) && self.accessBindingRef.uid.size() > 0",message="binding source requires project UID and UID-pinned context and access references" +type DNSResolverBindingSource struct { + ProjectUID types.UID `json:"projectUID"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` + AccessBindingRef DNSObjectReference `json:"accessBindingRef"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding placement is immutable" +type DNSResolverBindingPlacement struct { + Region string `json:"region"` + Shard string `json:"shard"` +} + +// +kubebuilder:validation:XValidation:rule="self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() > 0)",message="zone references must pin name and UID" +type DNSResolverBindingConfiguration struct { + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + Listeners DNSResolverBindingListeners `json:"listeners"` + // +kubebuilder:validation:MaxItems=256 + ZoneRefs []DNSObjectReference `json:"zoneRefs"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="listener identity is immutable for an access binding" +type DNSResolverBindingListeners struct { + Node DNSResolverBindingListener `json:"node"` + Regional DNSResolverBindingListener `json:"regional"` +} + +type DNSResolverBindingListener struct { + // +kubebuilder:validation:MinLength=1 + Address string `json:"address"` + // +kubebuilder:validation:Enum=53 + Port int32 `json:"port"` + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=2 + // +kubebuilder:validation:items:Enum=UDP;TCP + // +listType=set + Transports []string `json:"transports"` +} + +type DNSApplyAcknowledgement struct { + MemberID string `json:"memberID"` + Phase string `json:"phase"` + Revision int64 `json:"revision"` + WriterEpoch int64 `json:"writerEpoch,omitempty"` + SnapshotRevision int64 `json:"snapshotRevision,omitempty"` + AuthorizationIssuerEpoch int64 `json:"authorizationIssuerEpoch,omitempty"` + AuthorizationRevision int64 `json:"authorizationRevision,omitempty"` + ObservedUnixNano int64 `json:"observedUnixNano,omitempty"` + ObservedAt metav1.Time `json:"observedAt"` + ValidUntil *metav1.Time `json:"validUntil,omitempty"` + Error string `json:"error,omitempty"` +} + +type DNSResolverBindingStatus struct { + ObservedConfigurationRevision int64 `json:"observedConfigurationRevision,omitempty"` + Phase string `json:"phase,omitempty"` + MemberAcknowledgements []DNSApplyAcknowledgement `json:"memberAcknowledgements,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSResolverBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSResolverBindingSpec `json:"spec"` + Status DNSResolverBindingStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSResolverBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSResolverBinding `json:"items"` +} + +type DNSPublicationChunkReference struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int32 `json:"size"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication manifests are immutable" +type DNSPublicationManifestSpec struct { + ZoneRef DNSObjectReference `json:"zoneRef"` + ZoneApex string `json:"zoneApex"` + ContextUIDs []types.UID `json:"contextUIDs"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + PreviousRevision int64 `json:"previousRevision,omitempty"` + Tombstone bool `json:"tombstone,omitempty"` + Chunks []DNSPublicationChunkReference `json:"chunks"` + ContentHash string `json:"contentHash"` + GeneratedAt metav1.Time `json:"generatedAt"` + ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` + RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` + ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` +} + +type DNSPublicationTarget struct { + Region string `json:"region"` + Shard string `json:"shard"` +} + +type DNSRegistrationFence struct { + UID types.UID `json:"uid"` + Generation int64 `json:"generation"` +} + +// DNSContributionFence persists an authenticated high-water sequence even +// while its observation is unhealthy, rejected, or expired. +type DNSContributionFence struct { + UID types.UID `json:"uid"` + GrantUID types.UID `json:"grantUID"` + Epoch int64 `json:"epoch"` + Sequence int64 `json:"sequence"` + ValidUntil metav1.Time `json:"validUntil,omitempty"` +} + +type DNSPublicationManifestStatus struct { + ExportState string `json:"exportState,omitempty"` + ExportAttempts int32 `json:"exportAttempts,omitempty"` + ExportedAt *metav1.Time `json:"exportedAt,omitempty"` + StreamSequence uint64 `json:"streamSequence,omitempty"` + ReplicaAcknowledgements []DNSApplyAcknowledgement `json:"replicaAcknowledgements,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSPublicationManifest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationManifestSpec `json:"spec"` + Status DNSPublicationManifestStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSPublicationManifestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationManifest `json:"items"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication chunks are immutable" +type DNSPublicationChunkSpec struct { + ManifestUID types.UID `json:"manifestUID,omitempty"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + Index int32 `json:"index"` + SHA256 string `json:"sha256"` + Payload []byte `json:"payload"` +} + +// +kubebuilder:object:root=true +type DNSPublicationChunk struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationChunkSpec `json:"spec"` +} + +// +kubebuilder:object:root=true +type DNSPublicationChunkList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationChunk `json:"items"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="outbox entries are immutable" +type DNSTransportOutboxSpec struct { + Subject string `json:"subject"` + ResourceUID types.UID `json:"resourceUID"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + ManifestRef DNSObjectReference `json:"manifestRef"` + OwnershipRef DNSObjectReference `json:"ownershipRef,omitempty"` + PayloadHash string `json:"payloadHash"` + Payload []byte `json:"payload"` + Activation bool `json:"activation,omitempty"` + DependsOn []string `json:"dependsOn,omitempty"` +} + +type DNSTransportOutboxStatus struct { + State string `json:"state,omitempty"` + Attempts int32 `json:"attempts,omitempty"` + AcknowledgedAt *metav1.Time `json:"acknowledgedAt,omitempty"` + StreamSequence uint64 `json:"streamSequence,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:selectablefield:JSONPath=".status.state" +type DNSTransportOutbox struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSTransportOutboxSpec `json:"spec"` + Status DNSTransportOutboxStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSTransportOutboxList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSTransportOutbox `json:"items"` +} + +// DNSPublicationOwnership is a durable compare-and-swap record for a zone +// compiler. A lease takeover increments WriterEpoch and fences delayed holders. +type DNSPublicationOwnershipSpec struct { + ZoneUID types.UID `json:"zoneUID"` + HolderIdentity string `json:"holderIdentity"` + WriterEpoch int64 `json:"writerEpoch"` + NextRevision int64 `json:"nextRevision"` + LeaseUntil metav1.Time `json:"leaseUntil"` + LastContentHash string `json:"lastContentHash,omitempty"` + ActiveManifestName string `json:"activeManifestName,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSPublicationOwnership struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationOwnershipSpec `json:"spec"` +} + +// +kubebuilder:object:root=true +type DNSPublicationOwnershipList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationOwnership `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DNSResolverBinding{}, &DNSResolverBindingList{}, &DNSPublicationManifest{}, &DNSPublicationManifestList{}, &DNSPublicationChunk{}, &DNSPublicationChunkList{}, &DNSTransportOutbox{}, &DNSTransportOutboxList{}, &DNSPublicationOwnership{}, &DNSPublicationOwnershipList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9abcf217..aed412fe 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -10,6 +10,7 @@ import ( "go.datum.net/network-services-operator/api/v1alpha" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -103,6 +104,42 @@ func (in *DNSAdditionalNameRule) DeepCopy() *DNSAdditionalNameRule { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSApplyAcknowledgement) DeepCopyInto(out *DNSApplyAcknowledgement) { + *out = *in + in.ObservedAt.DeepCopyInto(&out.ObservedAt) + if in.ValidUntil != nil { + in, out := &in.ValidUntil, &out.ValidUntil + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSApplyAcknowledgement. +func (in *DNSApplyAcknowledgement) DeepCopy() *DNSApplyAcknowledgement { + if in == nil { + return nil + } + out := new(DNSApplyAcknowledgement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSContributionFence) DeepCopyInto(out *DNSContributionFence) { + *out = *in + in.ValidUntil.DeepCopyInto(&out.ValidUntil) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionFence. +func (in *DNSContributionFence) DeepCopy() *DNSContributionFence { + if in == nil { + return nil + } + out := new(DNSContributionFence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSContributionGrant) DeepCopyInto(out *DNSContributionGrant) { *out = *in @@ -474,6 +511,324 @@ func (in *DNSProducerPrincipal) DeepCopy() *DNSProducerPrincipal { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunk) DeepCopyInto(out *DNSPublicationChunk) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunk. +func (in *DNSPublicationChunk) DeepCopy() *DNSPublicationChunk { + if in == nil { + return nil + } + out := new(DNSPublicationChunk) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationChunk) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkList) DeepCopyInto(out *DNSPublicationChunkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationChunk, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkList. +func (in *DNSPublicationChunkList) DeepCopy() *DNSPublicationChunkList { + if in == nil { + return nil + } + out := new(DNSPublicationChunkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationChunkList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkReference) DeepCopyInto(out *DNSPublicationChunkReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkReference. +func (in *DNSPublicationChunkReference) DeepCopy() *DNSPublicationChunkReference { + if in == nil { + return nil + } + out := new(DNSPublicationChunkReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkSpec) DeepCopyInto(out *DNSPublicationChunkSpec) { + *out = *in + if in.Payload != nil { + in, out := &in.Payload, &out.Payload + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkSpec. +func (in *DNSPublicationChunkSpec) DeepCopy() *DNSPublicationChunkSpec { + if in == nil { + return nil + } + out := new(DNSPublicationChunkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifest) DeepCopyInto(out *DNSPublicationManifest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifest. +func (in *DNSPublicationManifest) DeepCopy() *DNSPublicationManifest { + if in == nil { + return nil + } + out := new(DNSPublicationManifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationManifest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestList) DeepCopyInto(out *DNSPublicationManifestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationManifest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestList. +func (in *DNSPublicationManifestList) DeepCopy() *DNSPublicationManifestList { + if in == nil { + return nil + } + out := new(DNSPublicationManifestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationManifestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestSpec) DeepCopyInto(out *DNSPublicationManifestSpec) { + *out = *in + out.ZoneRef = in.ZoneRef + if in.ContextUIDs != nil { + in, out := &in.ContextUIDs, &out.ContextUIDs + *out = make([]types.UID, len(*in)) + copy(*out, *in) + } + if in.Chunks != nil { + in, out := &in.Chunks, &out.Chunks + *out = make([]DNSPublicationChunkReference, len(*in)) + copy(*out, *in) + } + in.GeneratedAt.DeepCopyInto(&out.GeneratedAt) + if in.ContributionFences != nil { + in, out := &in.ContributionFences, &out.ContributionFences + *out = make([]DNSContributionFence, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RegistrationFences != nil { + in, out := &in.RegistrationFences, &out.RegistrationFences + *out = make([]DNSRegistrationFence, len(*in)) + copy(*out, *in) + } + if in.ServingTargets != nil { + in, out := &in.ServingTargets, &out.ServingTargets + *out = make([]DNSPublicationTarget, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestSpec. +func (in *DNSPublicationManifestSpec) DeepCopy() *DNSPublicationManifestSpec { + if in == nil { + return nil + } + out := new(DNSPublicationManifestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestStatus) DeepCopyInto(out *DNSPublicationManifestStatus) { + *out = *in + if in.ExportedAt != nil { + in, out := &in.ExportedAt, &out.ExportedAt + *out = (*in).DeepCopy() + } + if in.ReplicaAcknowledgements != nil { + in, out := &in.ReplicaAcknowledgements, &out.ReplicaAcknowledgements + *out = make([]DNSApplyAcknowledgement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestStatus. +func (in *DNSPublicationManifestStatus) DeepCopy() *DNSPublicationManifestStatus { + if in == nil { + return nil + } + out := new(DNSPublicationManifestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnership) DeepCopyInto(out *DNSPublicationOwnership) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnership. +func (in *DNSPublicationOwnership) DeepCopy() *DNSPublicationOwnership { + if in == nil { + return nil + } + out := new(DNSPublicationOwnership) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationOwnership) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnershipList) DeepCopyInto(out *DNSPublicationOwnershipList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationOwnership, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnershipList. +func (in *DNSPublicationOwnershipList) DeepCopy() *DNSPublicationOwnershipList { + if in == nil { + return nil + } + out := new(DNSPublicationOwnershipList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationOwnershipList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnershipSpec) DeepCopyInto(out *DNSPublicationOwnershipSpec) { + *out = *in + in.LeaseUntil.DeepCopyInto(&out.LeaseUntil) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnershipSpec. +func (in *DNSPublicationOwnershipSpec) DeepCopy() *DNSPublicationOwnershipSpec { + if in == nil { + return nil + } + out := new(DNSPublicationOwnershipSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationTarget) DeepCopyInto(out *DNSPublicationTarget) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationTarget. +func (in *DNSPublicationTarget) DeepCopy() *DNSPublicationTarget { + if in == nil { + return nil + } + out := new(DNSPublicationTarget) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordContribution) DeepCopyInto(out *DNSRecordContribution) { *out = *in @@ -721,6 +1076,21 @@ func (in *DNSRegistration) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistrationFence) DeepCopyInto(out *DNSRegistrationFence) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationFence. +func (in *DNSRegistrationFence) DeepCopy() *DNSRegistrationFence { + if in == nil { + return nil + } + out := new(DNSRegistrationFence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRegistrationList) DeepCopyInto(out *DNSRegistrationList) { *out = *in @@ -942,6 +1312,203 @@ func (in *DNSResolverAccessBindingStatus) DeepCopy() *DNSResolverAccessBindingSt return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBinding) DeepCopyInto(out *DNSResolverBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBinding. +func (in *DNSResolverBinding) DeepCopy() *DNSResolverBinding { + if in == nil { + return nil + } + out := new(DNSResolverBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverBinding) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingConfiguration) DeepCopyInto(out *DNSResolverBindingConfiguration) { + *out = *in + in.Listeners.DeepCopyInto(&out.Listeners) + if in.ZoneRefs != nil { + in, out := &in.ZoneRefs, &out.ZoneRefs + *out = make([]DNSObjectReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingConfiguration. +func (in *DNSResolverBindingConfiguration) DeepCopy() *DNSResolverBindingConfiguration { + if in == nil { + return nil + } + out := new(DNSResolverBindingConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingList) DeepCopyInto(out *DNSResolverBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSResolverBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingList. +func (in *DNSResolverBindingList) DeepCopy() *DNSResolverBindingList { + if in == nil { + return nil + } + out := new(DNSResolverBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverBindingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingListener) DeepCopyInto(out *DNSResolverBindingListener) { + *out = *in + if in.Transports != nil { + in, out := &in.Transports, &out.Transports + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingListener. +func (in *DNSResolverBindingListener) DeepCopy() *DNSResolverBindingListener { + if in == nil { + return nil + } + out := new(DNSResolverBindingListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingListeners) DeepCopyInto(out *DNSResolverBindingListeners) { + *out = *in + in.Node.DeepCopyInto(&out.Node) + in.Regional.DeepCopyInto(&out.Regional) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingListeners. +func (in *DNSResolverBindingListeners) DeepCopy() *DNSResolverBindingListeners { + if in == nil { + return nil + } + out := new(DNSResolverBindingListeners) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingPlacement) DeepCopyInto(out *DNSResolverBindingPlacement) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingPlacement. +func (in *DNSResolverBindingPlacement) DeepCopy() *DNSResolverBindingPlacement { + if in == nil { + return nil + } + out := new(DNSResolverBindingPlacement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingSource) DeepCopyInto(out *DNSResolverBindingSource) { + *out = *in + out.ResolverContextRef = in.ResolverContextRef + out.AccessBindingRef = in.AccessBindingRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingSource. +func (in *DNSResolverBindingSource) DeepCopy() *DNSResolverBindingSource { + if in == nil { + return nil + } + out := new(DNSResolverBindingSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingSpec) DeepCopyInto(out *DNSResolverBindingSpec) { + *out = *in + out.Source = in.Source + out.Placement = in.Placement + in.Configuration.DeepCopyInto(&out.Configuration) + in.Authorization.DeepCopyInto(&out.Authorization) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingSpec. +func (in *DNSResolverBindingSpec) DeepCopy() *DNSResolverBindingSpec { + if in == nil { + return nil + } + out := new(DNSResolverBindingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingStatus) DeepCopyInto(out *DNSResolverBindingStatus) { + *out = *in + if in.MemberAcknowledgements != nil { + in, out := &in.MemberAcknowledgements, &out.MemberAcknowledgements + *out = make([]DNSApplyAcknowledgement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingStatus. +func (in *DNSResolverBindingStatus) DeepCopy() *DNSResolverBindingStatus { + if in == nil { + return nil + } + out := new(DNSResolverBindingStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSResolverContext) DeepCopyInto(out *DNSResolverContext) { *out = *in @@ -1106,6 +1673,111 @@ func (in *DNSResolverQueryIdentity) DeepCopy() *DNSResolverQueryIdentity { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutbox) DeepCopyInto(out *DNSTransportOutbox) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutbox. +func (in *DNSTransportOutbox) DeepCopy() *DNSTransportOutbox { + if in == nil { + return nil + } + out := new(DNSTransportOutbox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSTransportOutbox) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxList) DeepCopyInto(out *DNSTransportOutboxList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSTransportOutbox, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxList. +func (in *DNSTransportOutboxList) DeepCopy() *DNSTransportOutboxList { + if in == nil { + return nil + } + out := new(DNSTransportOutboxList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSTransportOutboxList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxSpec) DeepCopyInto(out *DNSTransportOutboxSpec) { + *out = *in + out.ManifestRef = in.ManifestRef + out.OwnershipRef = in.OwnershipRef + if in.Payload != nil { + in, out := &in.Payload, &out.Payload + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxSpec. +func (in *DNSTransportOutboxSpec) DeepCopy() *DNSTransportOutboxSpec { + if in == nil { + return nil + } + out := new(DNSTransportOutboxSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxStatus) DeepCopyInto(out *DNSTransportOutboxStatus) { + *out = *in + if in.AcknowledgedAt != nil { + in, out := &in.AcknowledgedAt, &out.AcknowledgedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxStatus. +func (in *DNSTransportOutboxStatus) DeepCopy() *DNSTransportOutboxStatus { + if in == nil { + return nil + } + out := new(DNSTransportOutboxStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSZone) DeepCopyInto(out *DNSZone) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml new file mode 100644 index 00000000..72f5fe48 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationchunks.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationChunk + listKind: DNSPublicationChunkList + plural: dnspublicationchunks + singular: dnspublicationchunk + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + index: + format: int32 + type: integer + manifestUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + payload: + format: byte + type: string + revision: + format: int64 + type: integer + sha256: + type: string + writerEpoch: + format: int64 + type: integer + required: + - index + - payload + - revision + - sha256 + - writerEpoch + type: object + x-kubernetes-validations: + - message: publication chunks are immutable + rule: oldSelf == self + required: + - spec + type: object + served: true + storage: true diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml new file mode 100644 index 00000000..80dc8429 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml @@ -0,0 +1,293 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationmanifests.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationManifest + listKind: DNSPublicationManifestList + plural: dnspublicationmanifests + singular: dnspublicationmanifest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + chunks: + items: + properties: + name: + type: string + sha256: + type: string + size: + format: int32 + type: integer + required: + - name + - sha256 + - size + type: object + type: array + contentHash: + type: string + contextUIDs: + items: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + type: array + contributionFences: + items: + description: |- + DNSContributionFence persists an authenticated high-water sequence even + while its observation is unhealthy, rejected, or expired. + properties: + epoch: + format: int64 + type: integer + grantUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + sequence: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + validUntil: + format: date-time + type: string + required: + - epoch + - grantUID + - sequence + - uid + type: object + type: array + generatedAt: + format: date-time + type: string + previousRevision: + format: int64 + type: integer + registrationFences: + items: + properties: + generation: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - generation + - uid + type: object + type: array + revision: + format: int64 + type: integer + servingTargets: + items: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + type: array + tombstone: + type: boolean + writerEpoch: + format: int64 + type: integer + zoneApex: + type: string + zoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - chunks + - contentHash + - contextUIDs + - generatedAt + - revision + - writerEpoch + - zoneApex + - zoneRef + type: object + x-kubernetes-validations: + - message: publication manifests are immutable + rule: oldSelf == self + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + exportAttempts: + format: int32 + type: integer + exportState: + type: string + exportedAt: + format: date-time + type: string + replicaAcknowledgements: + items: + properties: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + streamSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml new file mode 100644 index 00000000..2e9ae926 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationownerships.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationOwnership + listKind: DNSPublicationOwnershipList + plural: dnspublicationownerships + singular: dnspublicationownership + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + DNSPublicationOwnership is a durable compare-and-swap record for a zone + compiler. A lease takeover increments WriterEpoch and fences delayed holders. + properties: + activeManifestName: + type: string + holderIdentity: + type: string + lastContentHash: + type: string + leaseUntil: + format: date-time + type: string + nextRevision: + format: int64 + type: integer + writerEpoch: + format: int64 + type: integer + zoneUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - holderIdentity + - leaseUntil + - nextRevision + - writerEpoch + - zoneUID + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml new file mode 100644 index 00000000..adab7d82 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -0,0 +1,378 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsresolverbindings.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverBinding + listKind: DNSResolverBindingList + plural: dnsresolverbindings + singular: dnsresolverbinding + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DNSResolverBinding is the DNS-service projection of a consumer + context and access binding. + properties: + authorization: + properties: + sequence: + format: int64 + minimum: 1 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + minimum: 1 + type: integer + required: + - sequence + - validUntil + - writerEpoch + type: object + configuration: + properties: + generation: + format: int64 + minimum: 1 + type: integer + listeners: + properties: + node: + properties: + address: + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + regional: + properties: + address: + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + required: + - node + - regional + type: object + x-kubernetes-validations: + - message: listener identity is immutable for an access binding + rule: oldSelf == self + revision: + format: int64 + minimum: 1 + type: integer + zoneRefs: + items: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + maxItems: 256 + type: array + required: + - generation + - listeners + - revision + - zoneRefs + type: object + x-kubernetes-validations: + - message: zone references must pin name and UID + rule: self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() + > 0) + placement: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + x-kubernetes-validations: + - message: binding placement is immutable + rule: oldSelf == self + source: + properties: + accessBindingRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + projectUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + resolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - accessBindingRef + - projectUID + - resolverContextRef + type: object + x-kubernetes-validations: + - message: binding source identity is immutable + rule: oldSelf == self + - message: binding source requires project UID and UID-pinned context + and access references + rule: self.projectUID.size() > 0 && self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() + > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) + && self.accessBindingRef.uid.size() > 0 + tombstone: + type: boolean + required: + - authorization + - configuration + - placement + - source + type: object + x-kubernetes-validations: + - message: configuration fences must not decrease + rule: self.configuration.generation >= oldSelf.configuration.generation + && self.configuration.revision >= oldSelf.configuration.revision + - message: authorization fences must not decrease + rule: self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence >= oldSelf.authorization.sequence) + - message: changing an authorization deadline requires a new fence + rule: self.authorization.writerEpoch != oldSelf.authorization.writerEpoch + || self.authorization.sequence != oldSelf.authorization.sequence || + self.authorization.validUntil == oldSelf.authorization.validUntil + - message: zone membership changes require a new cache generation and + configuration revision + rule: self.configuration.zoneRefs == oldSelf.configuration.zoneRefs + || (self.configuration.generation > oldSelf.configuration.generation + && self.configuration.revision > oldSelf.configuration.revision) + - message: changing withdrawal state requires a new configuration revision + rule: (has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) + && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision + - message: reactivating withdrawn access requires a new source authorization + fence + rule: '!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) + && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence > oldSelf.authorization.sequence)' + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + memberAcknowledgements: + items: + properties: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + observedConfigurationRevision: + format: int64 + type: integer + phase: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml new file mode 100644 index 00000000..dab793c6 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml @@ -0,0 +1,139 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnstransportoutboxes.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSTransportOutbox + listKind: DNSTransportOutboxList + plural: dnstransportoutboxes + singular: dnstransportoutbox + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + activation: + type: boolean + dependsOn: + items: + type: string + type: array + manifestRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + ownershipRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + payload: + format: byte + type: string + payloadHash: + type: string + resourceUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + revision: + format: int64 + type: integer + subject: + type: string + writerEpoch: + format: int64 + type: integer + required: + - manifestRef + - payload + - payloadHash + - resourceUID + - revision + - subject + - writerEpoch + type: object + x-kubernetes-validations: + - message: outbox entries are immutable + rule: oldSelf == self + status: + properties: + acknowledgedAt: + format: date-time + type: string + attempts: + format: int32 + type: integer + lastError: + type: string + state: + type: string + streamSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .status.state + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b6efcd04..760dc379 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,11 @@ resources: - bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml - bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml - bases/dns.networking.miloapis.com_dnszoneassociations.yaml +- bases/dns.networking.miloapis.com_dnspublicationchunks.yaml +- bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml +- bases/dns.networking.miloapis.com_dnspublicationownerships.yaml +- bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +- bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml # +kubebuilder:scaffold:crdkustomizeresource # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/go.mod b/go.mod index ff712e5a..f7a6b5ad 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/klauspost/compress v1.18.7 // indirect + github.com/klauspost/compress v1.20.0 // indirect github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect @@ -67,6 +67,8 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect @@ -123,6 +125,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.54.0 github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.67.5 // indirect diff --git a/go.sum b/go.sum index 078c70a3..f4489ec8 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,6 @@ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6O github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -155,8 +153,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -165,8 +161,8 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -213,6 +209,12 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -301,44 +303,28 @@ go.datum.net/datumctl v0.18.1 h1:MP83cRkJ94aq4lYyOm5dSLU60JNnmR486LIeZnaCw8s= go.datum.net/datumctl v0.18.1/go.mod h1:0L0BBgFfud8ZuV9sbsSmeEBUPoK2q9g4boU5yczF92o= go.datum.net/network-services-operator v0.9.0 h1:D+q4iXFlQTeZxPJBxPW7hDJW9sHLMAiVhUjjJKT2R98= go.datum.net/network-services-operator v0.9.0/go.mod h1:RvP9wWGKSlWlASp1pe/vgvaExm+BsD6hq9XJzElN6dY= -go.miloapis.com/milo v0.31.0 h1:gBZ9NaplTCFhgWUZJ18N38JsFDCVtdKmypwJvffSdUU= -go.miloapis.com/milo v0.31.0/go.mod h1:GKK3afjCwshfZfvhjNe1wp/H45z4m7x5oG/8xbSgU1M= go.miloapis.com/milo v0.35.0 h1:pXXqEm8fHeMOS/qBGw2501ftfI3e4nUA/zMZQqUvlmA= go.miloapis.com/milo v0.35.0/go.mod h1:zMzwGXexUlPlFaZ9/4U+2DDbzPj4xzs9I76Z901g2sE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -355,16 +341,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -373,8 +355,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -384,8 +364,6 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -401,16 +379,12 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -419,8 +393,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -438,12 +410,8 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= diff --git a/internal/internaldns/transport/jetstream.go b/internal/internaldns/transport/jetstream.go new file mode 100644 index 00000000..7aa00976 --- /dev/null +++ b/internal/internaldns/transport/jetstream.go @@ -0,0 +1,434 @@ +// Package transport provides the authenticated, durable regional event bus for +// internal DNS. Every serving member must use a +// different Durable name; sharing it would divide updates between replicas. +package transport + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "go.miloapis.com/dns-operator/internal/internaldns/model" +) + +type TLSConfig struct { + CAFile string `json:"caFile,omitempty"` + CertificateFile string `json:"certificateFile,omitempty"` + KeyFile string `json:"keyFile,omitempty"` + ServerName string `json:"serverName,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` +} + +type Config struct { + URL string `json:"url"` + Name string `json:"name"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + PasswordFile string `json:"passwordFile,omitempty"` + Token string `json:"token,omitempty"` + CredentialsFile string `json:"credentialsFile,omitempty"` + TLS *TLSConfig `json:"tls,omitempty"` + // AllowInsecure is intended for isolated development fixtures only. A + // production caller must configure TLS and an authentication method. + AllowInsecure bool `json:"allowInsecure,omitempty"` + ConnectTimeout time.Duration `json:"connectTimeout,omitempty"` + PublishTimeout time.Duration `json:"publishTimeout,omitempty"` +} + +func (c Config) validate() error { + if c.URL == "" || c.Name == "" { + return errors.New("NATS URL and connection name are required") + } + if !c.AllowInsecure && c.TLS == nil { + return errors.New("NATS TLS is required unless AllowInsecure is explicitly set") + } + if c.Password != "" && c.PasswordFile != "" { + return errors.New("NATS password and password file are mutually exclusive") + } + if (c.Password != "" || c.PasswordFile != "") && c.Username == "" { + return errors.New("NATS username is required with a password source") + } + if c.Username != "" && c.Password == "" && c.PasswordFile == "" { + return errors.New("NATS username requires a password source") + } + if c.AllowInsecure && c.CredentialsFile == "" && c.Token == "" && c.Username == "" { + return nil + } + auth := 0 + if c.CredentialsFile != "" { + auth++ + } + if c.Token != "" { + auth++ + } + if c.Username != "" { + auth++ + } + if auth != 1 { + return errors.New("exactly one NATS authentication method is required") + } + return nil +} + +type JetStream struct { + nc *nats.Conn + js jetstream.JetStream + publishTimeout time.Duration +} + +func Connect(c Config) (*JetStream, error) { + if err := c.validate(); err != nil { + return nil, err + } + opts := []nats.Option{ + nats.Name(c.Name), + nats.Timeout(defaultDuration(c.ConnectTimeout, 10*time.Second)), + // A regional partition must not permanently close a long-running agent. + // Callers keep retrying while local expiry and the watchdog gate serving. + nats.MaxReconnects(-1), + } + if c.CredentialsFile != "" { + opts = append(opts, nats.UserCredentials(c.CredentialsFile)) + } + if c.Token != "" { + opts = append(opts, nats.Token(c.Token)) + } + if c.Username != "" { + password := c.Password + if c.PasswordFile != "" { + value, err := os.ReadFile(c.PasswordFile) + if err != nil { + return nil, fmt.Errorf("read NATS password: %w", err) + } + password = strings.TrimSpace(string(value)) + if password == "" { + return nil, errors.New("NATS password file is empty") + } + } + opts = append(opts, nats.UserInfo(c.Username, password)) + } + if c.TLS != nil { + tlsConfig, err := loadTLS(*c.TLS) + if err != nil { + return nil, err + } + opts = append(opts, nats.Secure(tlsConfig)) + } + nc, err := nats.Connect(c.URL, opts...) + if err != nil { + return nil, fmt.Errorf("connect NATS: %w", err) + } + js, err := jetstream.New(nc) + if err != nil { + nc.Close() + return nil, fmt.Errorf("open JetStream: %w", err) + } + return &JetStream{nc: nc, js: js, publishTimeout: defaultDuration(c.PublishTimeout, 5*time.Second)}, nil +} + +func loadTLS(c TLSConfig) (*tls.Config, error) { + if c.InsecureSkipVerify { + return nil, errors.New("NATS TLS InsecureSkipVerify is forbidden") + } + t := &tls.Config{MinVersion: tls.VersionTLS13, ServerName: c.ServerName} + if c.CAFile != "" { + pem, err := os.ReadFile(c.CAFile) + if err != nil { + return nil, fmt.Errorf("read NATS CA: %w", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + return nil, errors.New("NATS CA file contains no certificates") + } + t.RootCAs = pool + } + if c.CertificateFile != "" || c.KeyFile != "" { + if c.CertificateFile == "" || c.KeyFile == "" { + return nil, errors.New("NATS client certificate and key must be configured together") + } + cert, err := tls.LoadX509KeyPair(c.CertificateFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("load NATS client certificate: %w", err) + } + t.Certificates = []tls.Certificate{cert} + } + return t, nil +} + +func (j *JetStream) Close() error { + if j == nil || j.nc == nil { + return nil + } + if err := j.nc.Drain(); err != nil { + j.nc.Close() + return err + } + return nil +} + +type PublishAck struct { + Stream string + Sequence uint64 + Duplicate bool +} + +// ExportPublisher is implemented by JetStream and used by the durable outbox +// controller. EventID supplies JetStream de-duplication for ambiguous retries. +type ExportPublisher interface { + Publish(context.Context, string, model.Envelope) (PublishAck, error) +} + +func (j *JetStream) Publish(ctx context.Context, subject string, env model.Envelope) (PublishAck, error) { + if err := env.Validate(); err != nil { + return PublishAck{}, err + } + if subject == "" { + return PublishAck{}, errors.New("publish subject is required") + } + if j == nil || j.nc == nil || j.nc.IsClosed() { + return PublishAck{}, nats.ErrConnectionClosed + } + if !j.nc.IsConnected() { + return PublishAck{}, nats.ErrDisconnected + } + b, err := json.Marshal(env) + if err != nil { + return PublishAck{}, err + } + publishCtx, cancel := boundedContext(ctx, defaultDuration(j.publishTimeout, 5*time.Second)) + defer cancel() + ack, err := j.js.Publish(publishCtx, subject, b, jetstream.WithMsgID(brokerMessageID(subject, env.EventID))) + if err != nil { + return PublishAck{}, fmt.Errorf("publish %s: %w", subject, err) + } + return PublishAck{Stream: ack.Stream, Sequence: ack.Sequence, Duplicate: ack.Duplicate}, nil +} + +func brokerMessageID(subject, eventID string) string { + // JetStream de-duplication spans every subject in a stream. Namespace the + // application event ID so an ACK publisher cannot reserve a controller + // publication's ID on its independently authorized subject. + return model.Hash([]byte(subject + "\x00" + eventID)) +} + +func boundedContext(parent context.Context, limit time.Duration) (context.Context, context.CancelFunc) { + if deadline, ok := parent.Deadline(); ok && time.Until(deadline) <= limit { + return context.WithCancel(parent) + } + return context.WithTimeout(parent, limit) +} + +type StreamConfig struct { + Name string `json:"name"` + Subjects []string `json:"subjects,omitempty"` + MaxAge time.Duration `json:"maxAge,omitempty"` + MaxBytes int64 `json:"maxBytes,omitempty"` + Replicas int `json:"replicas,omitempty"` + SnapshotsBucket string `json:"snapshotsBucket"` +} + +func (j *JetStream) EnsureStream(ctx context.Context, c StreamConfig) error { + if c.Name == "" || len(c.Subjects) == 0 { + return errors.New("stream name and subjects are required") + } + _, err := j.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{Name: c.Name, Subjects: c.Subjects, Retention: jetstream.LimitsPolicy, Storage: jetstream.FileStorage, MaxAge: c.MaxAge, MaxBytes: c.MaxBytes, Replicas: c.Replicas, Duplicates: 2 * time.Minute}) + if err != nil { + return fmt.Errorf("ensure stream %q: %w", c.Name, err) + } + if c.SnapshotsBucket != "" { + _, err = j.js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: c.SnapshotsBucket, Description: "current complete internal DNS bootstrap snapshots", History: 1, Storage: jetstream.FileStorage, Replicas: c.Replicas}) + if err != nil { + return fmt.Errorf("ensure snapshot bucket %q: %w", c.SnapshotsBucket, err) + } + } + return nil +} + +type ConsumerConfig struct { + Stream string `json:"stream"` + Durable string `json:"durable"` + FilterSubjects []string `json:"filterSubjects"` + AckWait time.Duration `json:"ackWait,omitempty"` + MaxAckPending int `json:"maxAckPending,omitempty"` +} + +type Message interface { + Subject() string + Envelope() model.Envelope + StreamSequence() uint64 + Ack(context.Context) error + Retry(time.Duration) error + Reject(string) error +} + +type Consumer struct{ c jetstream.Consumer } + +func (j *JetStream) Consumer(ctx context.Context, c ConsumerConfig) (*Consumer, error) { + if c.Stream == "" || !validDurable(c.Durable) || len(c.FilterSubjects) == 0 { + return nil, errors.New("stream, safe durable name, and filters are required") + } + cons, err := j.js.CreateOrUpdateConsumer(ctx, c.Stream, jetstream.ConsumerConfig{Durable: c.Durable, AckPolicy: jetstream.AckExplicitPolicy, DeliverPolicy: jetstream.DeliverAllPolicy, ReplayPolicy: jetstream.ReplayInstantPolicy, FilterSubjects: c.FilterSubjects, AckWait: defaultDuration(c.AckWait, 30*time.Second), MaxAckPending: defaultInt(c.MaxAckPending, 64)}) + if err != nil { + return nil, fmt.Errorf("ensure durable consumer %q: %w", c.Durable, err) + } + return &Consumer{c: cons}, nil +} + +// Next returns one event. Malformed envelopes are terminated so a poison event +// cannot block every later revision on an ordered durable. +func (c *Consumer) Next(ctx context.Context) (Message, error) { + m, err := c.c.Next(jetstream.FetchContext(ctx)) + if err != nil { + return nil, err + } + var env model.Envelope + if err := json.Unmarshal(m.Data(), &env); err != nil { + _ = m.TermWithReason("invalid JSON envelope") + return nil, fmt.Errorf("decode envelope: %w", err) + } + if err := env.Validate(); err != nil { + _ = m.TermWithReason("invalid DNS envelope") + return nil, err + } + meta, err := m.Metadata() + if err != nil { + _ = m.Nak() + return nil, err + } + return &message{msg: m, subject: m.Subject(), env: env, seq: meta.Sequence.Stream}, nil +} + +type message struct { + msg jetstream.Msg + subject string + env model.Envelope + seq uint64 +} + +func (m *message) Subject() string { return m.subject } +func (m *message) Envelope() model.Envelope { return m.env } +func (m *message) StreamSequence() uint64 { return m.seq } +func (m *message) Ack(ctx context.Context) error { return m.msg.DoubleAck(ctx) } +func (m *message) Retry(d time.Duration) error { return m.msg.NakWithDelay(d) } +func (m *message) Reject(reason string) error { return m.msg.TermWithReason(reason) } + +type SnapshotStore struct{ kv jetstream.KeyValue } + +func (j *JetStream) SnapshotStore(ctx context.Context, bucket string) (*SnapshotStore, error) { + if bucket == "" { + return nil, errors.New("snapshot bucket is required") + } + kv, err := j.js.KeyValue(ctx, bucket) + if err != nil { + return nil, fmt.Errorf("open snapshot bucket %q: %w", bucket, err) + } + return &SnapshotStore{kv: kv}, nil +} + +// Put stores a complete, already validated bootstrap envelope. It uses a KV +// compare-and-swap loop so a delayed publisher cannot replace a current +// bootstrap pointer with an older epoch or revision. +func (s *SnapshotStore) Put(ctx context.Context, key string, env model.Envelope) (uint64, error) { + if err := env.Validate(); err != nil { + return 0, err + } + if !validSnapshotKey(key) { + return 0, fmt.Errorf("invalid snapshot key %q", key) + } + b, err := json.Marshal(env) + if err != nil { + return 0, err + } + for attempt := 0; attempt < 8; attempt++ { + current, err := s.kv.Get(ctx, key) + if errors.Is(err, jetstream.ErrKeyNotFound) { + revision, err := s.kv.Create(ctx, key, b) + if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) { + continue + } + return revision, err + } + if err != nil { + return 0, err + } + var old model.Envelope + if err := json.Unmarshal(current.Value(), &old); err != nil { + return 0, fmt.Errorf("decode current snapshot %q: %w", key, err) + } + cmp := model.Compare(env.Epoch, env.Revision, old.Epoch, old.Revision) + if cmp < 0 { + return current.Revision(), nil + } + if cmp == 0 { + if model.Hash(current.Value()) != model.Hash(b) { + return 0, fmt.Errorf("snapshot %q has conflicting content at epoch %d revision %d", key, env.Epoch, env.Revision) + } + return current.Revision(), nil + } + revision, err := s.kv.Update(ctx, key, b, current.Revision()) + if errors.Is(err, jetstream.ErrKeyRevisionMismatch) { + continue + } + return revision, err + } + return 0, fmt.Errorf("snapshot %q CAS did not converge", key) +} + +func (s *SnapshotStore) Get(ctx context.Context, key string) (model.Envelope, error) { + entry, err := s.kv.Get(ctx, key) + if err != nil { + return model.Envelope{}, err + } + var env model.Envelope + if err := json.Unmarshal(entry.Value(), &env); err != nil { + return model.Envelope{}, err + } + return env, env.Validate() +} + +func (s *SnapshotStore) List(ctx context.Context, prefix string) ([]model.Envelope, error) { + keys, err := s.kv.Keys(ctx) + if err != nil { + return nil, err + } + var out []model.Envelope + for _, key := range keys { + if !strings.HasPrefix(key, prefix) { + continue + } + env, err := s.Get(ctx, key) + if err != nil { + return nil, fmt.Errorf("read snapshot %q: %w", key, err) + } + out = append(out, env) + } + return out, nil +} + +func validDurable(s string) bool { + if s == "" { + return false + } + return !strings.ContainsAny(s, " .*\\/>\t\r\n") +} +func validSnapshotKey(s string) bool { return s != "" && !strings.ContainsAny(s, " *>\t\r\n/") } +func defaultDuration(v, d time.Duration) time.Duration { + if v <= 0 { + return d + } + return v +} +func defaultInt(v, d int) int { + if v <= 0 { + return d + } + return v +} diff --git a/internal/internaldns/transport/jetstream_test.go b/internal/internaldns/transport/jetstream_test.go new file mode 100644 index 00000000..8a9e1456 --- /dev/null +++ b/internal/internaldns/transport/jetstream_test.go @@ -0,0 +1,55 @@ +package transport + +import "testing" + +func TestProductionTransportRequiresTLSAndOneAuthMethod(t *testing.T) { + t.Parallel() + base := Config{URL: "nats://example:4222", Name: "agent"} + if err := base.validate(); err == nil { + t.Fatal("plaintext production NATS accepted") + } + base.TLS = &TLSConfig{ServerName: "nats.example"} + if err := base.validate(); err == nil { + t.Fatal("unauthenticated production NATS accepted") + } + base.Token = "secret" + if err := base.validate(); err != nil { + t.Fatal(err) + } + base.Username, base.Password = "also", "configured" + if err := base.validate(); err == nil { + t.Fatal("multiple authentication methods accepted") + } +} + +func TestDevelopmentTransportMustBeExplicit(t *testing.T) { + t.Parallel() + if err := (Config{URL: "nats://127.0.0.1:4222", Name: "e2e", AllowInsecure: true}).validate(); err != nil { + t.Fatal(err) + } +} + +func TestDurableNameRejectsQueueSharingSyntax(t *testing.T) { + t.Parallel() + for _, bad := range []string{"", "replica.one", "replica one", "replica*"} { + if validDurable(bad) { + t.Fatalf("unsafe durable %q accepted", bad) + } + } + if !validDurable("auth-replica-one") { + t.Fatal("safe durable rejected") + } +} + +func TestBrokerMessageIDIsNamespacedBySubject(t *testing.T) { + t.Parallel() + eventID := "event-123" + publication := brokerMessageID("internaldns.pub.us-east.s1.manifest.zone-a", eventID) + ack := brokerMessageID("internaldns.ack.us-east.s1.member-a", eventID) + if publication == ack { + t.Fatal("the same event ID collided across independently authorized subjects") + } + if publication != brokerMessageID("internaldns.pub.us-east.s1.manifest.zone-a", eventID) { + t.Fatal("message ID is not stable for an ambiguous retry") + } +} From ddb235eaa18f8d95f5c02f2b163f7e8ddd469012 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:07:40 -0500 Subject: [PATCH 2/4] Validate publication delivery contracts with schemas and CEL --- api/v1alpha1/internaldns_service_types.go | 80 +++++++++----- ...rnaldns_service_validation_envtest_test.go | 100 ++++++++++++++++++ .../internaldns_validation_envtest_test.go | 2 + ...ing.miloapis.com_dnspublicationchunks.yaml | 5 + ....miloapis.com_dnspublicationmanifests.yaml | 20 ++++ ...miloapis.com_dnspublicationownerships.yaml | 16 +++ ...king.miloapis.com_dnsresolverbindings.yaml | 16 +++ ...ing.miloapis.com_dnstransportoutboxes.yaml | 6 ++ 8 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 api/v1alpha1/internaldns_service_validation_envtest_test.go diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go index b55071d0..e97eb874 100644 --- a/api/v1alpha1/internaldns_service_types.go +++ b/api/v1alpha1/internaldns_service_types.go @@ -25,6 +25,8 @@ type DNSResolverBindingSpec struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding source identity is immutable" // +kubebuilder:validation:XValidation:rule="self.projectUID.size() > 0 && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) && self.accessBindingRef.uid.size() > 0",message="binding source requires project UID and UID-pinned context and access references" type DNSResolverBindingSource struct { + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MaxLength=128 ProjectUID types.UID `json:"projectUID"` ResolverContextRef DNSObjectReference `json:"resolverContextRef"` AccessBindingRef DNSObjectReference `json:"accessBindingRef"` @@ -32,8 +34,12 @@ type DNSResolverBindingSource struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding placement is immutable" type DNSResolverBindingPlacement struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Region string `json:"region"` - Shard string `json:"shard"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Shard string `json:"shard"` } // +kubebuilder:validation:XValidation:rule="self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() > 0)",message="zone references must pin name and UID" @@ -55,6 +61,7 @@ type DNSResolverBindingListeners struct { type DNSResolverBindingListener struct { // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Address string `json:"address"` // +kubebuilder:validation:Enum=53 Port int32 `json:"port"` @@ -103,26 +110,36 @@ type DNSResolverBindingList struct { } type DNSPublicationChunkReference struct { - Name string `json:"name"` + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` SHA256 string `json:"sha256"` - Size int32 `json:"size"` + // +kubebuilder:validation:Minimum=1 + Size int32 `json:"size"` } // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication manifests are immutable" +// +kubebuilder:validation:XValidation:rule="self.zoneRef.name.size() > 0 && has(self.zoneRef.uid) && self.zoneRef.uid.size() > 0",message="zoneRef requires name and UID" +// +kubebuilder:validation:XValidation:rule="!has(self.previousRevision) || (self.previousRevision >= 0 && self.previousRevision < self.revision)",message="previousRevision must precede revision" +// +kubebuilder:validation:XValidation:rule="(has(self.tombstone) && self.tombstone) ? (self.chunks.size() == 0 && self.contentHash == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') : self.chunks.size() > 0",message="withdrawal requires empty content; publication requires chunks" type DNSPublicationManifestSpec struct { - ZoneRef DNSObjectReference `json:"zoneRef"` - ZoneApex string `json:"zoneApex"` - ContextUIDs []types.UID `json:"contextUIDs"` - WriterEpoch int64 `json:"writerEpoch"` - Revision int64 `json:"revision"` - PreviousRevision int64 `json:"previousRevision,omitempty"` - Tombstone bool `json:"tombstone,omitempty"` - Chunks []DNSPublicationChunkReference `json:"chunks"` - ContentHash string `json:"contentHash"` - GeneratedAt metav1.Time `json:"generatedAt"` - ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` - RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` - ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` + ZoneRef DNSObjectReference `json:"zoneRef"` + // +kubebuilder:validation:MinLength=1 + ZoneApex string `json:"zoneApex"` + ContextUIDs []types.UID `json:"contextUIDs"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + PreviousRevision int64 `json:"previousRevision,omitempty"` + Tombstone bool `json:"tombstone,omitempty"` + Chunks []DNSPublicationChunkReference `json:"chunks"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` + ContentHash string `json:"contentHash"` + GeneratedAt metav1.Time `json:"generatedAt"` + ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` + RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` + ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` } type DNSPublicationTarget struct { @@ -173,11 +190,16 @@ type DNSPublicationManifestList struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication chunks are immutable" type DNSPublicationChunkSpec struct { ManifestUID types.UID `json:"manifestUID,omitempty"` - WriterEpoch int64 `json:"writerEpoch"` - Revision int64 `json:"revision"` - Index int32 `json:"index"` - SHA256 string `json:"sha256"` - Payload []byte `json:"payload"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + // +kubebuilder:validation:Minimum=0 + Index int32 `json:"index"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` + SHA256 string `json:"sha256"` + // +kubebuilder:validation:MinLength=1 + Payload []byte `json:"payload"` } // +kubebuilder:object:root=true @@ -235,10 +257,20 @@ type DNSTransportOutboxList struct { // DNSPublicationOwnership is a durable compare-and-swap record for a zone // compiler. A lease takeover increments WriterEpoch and fences delayed holders. +// +kubebuilder:validation:XValidation:rule="self.writerEpoch >= oldSelf.writerEpoch && self.nextRevision >= oldSelf.nextRevision",message="publication ownership fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.holderIdentity == oldSelf.holderIdentity || self.writerEpoch > oldSelf.writerEpoch",message="ownership transfer requires a new writer epoch" type DNSPublicationOwnershipSpec struct { - ZoneUID types.UID `json:"zoneUID"` - HolderIdentity string `json:"holderIdentity"` - WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="zoneUID is immutable" + ZoneUID types.UID `json:"zoneUID"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 + HolderIdentity string `json:"holderIdentity"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 NextRevision int64 `json:"nextRevision"` LeaseUntil metav1.Time `json:"leaseUntil"` LastContentHash string `json:"lastContentHash,omitempty"` diff --git a/api/v1alpha1/internaldns_service_validation_envtest_test.go b/api/v1alpha1/internaldns_service_validation_envtest_test.go new file mode 100644 index 00000000..3c1c1efd --- /dev/null +++ b/api/v1alpha1/internaldns_service_validation_envtest_test.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "k8s.io/apimachinery/pkg/types" + "strings" + "testing" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func validateServiceDNSContracts(t *testing.T, cl client.Client) { + t.Helper() + ctx := context.Background() + invalid := func(t *testing.T, err error) { + t.Helper() + if !apierrors.IsInvalid(err) { + t.Fatalf("expected API validation rejection, got %v", err) + } + } + manifest := func(name string) *DNSPublicationManifest { + return &DNSPublicationManifest{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSPublicationManifestSpec{ZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, ZoneApex: "datum.internal", ContextUIDs: []types.UID{}, WriterEpoch: 1, Revision: 2, PreviousRevision: 1, Chunks: []DNSPublicationChunkReference{{Name: "chunk", SHA256: strings.Repeat("a", 64), Size: 8}}, ContentHash: strings.Repeat("b", 64), GeneratedAt: metav1.Now()}} + } + for _, tc := range []struct { + name string + mutate func(*DNSPublicationManifest) + }{ + {"manifest-zero-epoch", func(a *DNSPublicationManifest) { a.Spec.WriterEpoch = 0 }}, + {"manifest-zero-revision", func(a *DNSPublicationManifest) { a.Spec.Revision = 0 }}, + {"manifest-future-predecessor", func(a *DNSPublicationManifest) { a.Spec.PreviousRevision = a.Spec.Revision }}, + {"manifest-invalid-hash", func(a *DNSPublicationManifest) { a.Spec.ContentHash = "invalid" }}, + {"manifest-empty-publication", func(a *DNSPublicationManifest) { a.Spec.Chunks = []DNSPublicationChunkReference{} }}, + {"manifest-withdrawal-content", func(a *DNSPublicationManifest) { a.Spec.Tombstone = true }}, + {"manifest-empty-chunk", func(a *DNSPublicationManifest) { a.Spec.Chunks[0].Size = 0 }}, + } { + t.Run(tc.name, func(t *testing.T) { a := manifest(tc.name); tc.mutate(a); invalid(t, cl.Create(ctx, a)) }) + } + t.Run("manifest-status-and-immutability", func(t *testing.T) { + a := manifest("manifest") + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Status.ExportState = "Delivered" + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.Revision++ + invalid(t, cl.Update(ctx, a)) + }) + t.Run("manifest-withdrawal", func(t *testing.T) { + a := manifest("withdrawal") + a.Spec.Tombstone = true + a.Spec.Chunks = []DNSPublicationChunkReference{} + a.Spec.ContentHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + }) + t.Run("chunk-empty-payload", func(t *testing.T) { + a := &DNSPublicationChunk{ObjectMeta: metav1.ObjectMeta{Name: "empty-chunk", Namespace: "validation"}, Spec: DNSPublicationChunkSpec{WriterEpoch: 1, Revision: 1, Index: 0, SHA256: strings.Repeat("a", 64), Payload: []byte{}}} + invalid(t, cl.Create(ctx, a)) + }) + owner := func(name string) *DNSPublicationOwnership { + return &DNSPublicationOwnership{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSPublicationOwnershipSpec{ZoneUID: "zone-uid", HolderIdentity: "worker-a", WriterEpoch: 2, NextRevision: 3, LeaseUntil: metav1.NewTime(time.Now().Add(time.Minute))}} + } + for _, tc := range []struct { + name string + mutate func(*DNSPublicationOwnership) + }{ + {"owner-change-zone", func(a *DNSPublicationOwnership) { a.Spec.ZoneUID = "other" }}, + {"owner-epoch-rollback", func(a *DNSPublicationOwnership) { a.Spec.WriterEpoch-- }}, + {"owner-revision-rollback", func(a *DNSPublicationOwnership) { a.Spec.NextRevision-- }}, + {"owner-unfenced-transfer", func(a *DNSPublicationOwnership) { a.Spec.HolderIdentity = "worker-b" }}, + } { + t.Run(tc.name, func(t *testing.T) { + a := owner(tc.name) + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + tc.mutate(a) + invalid(t, cl.Update(ctx, a)) + }) + } + t.Run("owner-fenced-transfer", func(t *testing.T) { + a := owner("transfer") + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.HolderIdentity = "worker-b" + a.Spec.WriterEpoch++ + if err := cl.Update(ctx, a); err != nil { + t.Fatal(err) + } + }) +} diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index a4f9578e..4426872b 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -143,4 +143,6 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { a.Spec.GrantRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) + validateServiceDNSContracts(t, cl) + } diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml index 72f5fe48..ef9d0efb 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml @@ -39,6 +39,7 @@ spec: properties: index: format: int32 + minimum: 0 type: integer manifestUID: description: |- @@ -48,14 +49,18 @@ spec: type: string payload: format: byte + minLength: 1 type: string revision: format: int64 + minimum: 1 type: integer sha256: + pattern: ^[a-fA-F0-9]{64}$ type: string writerEpoch: format: int64 + minimum: 1 type: integer required: - index diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml index 80dc8429..1a8845d5 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml @@ -41,11 +41,14 @@ spec: items: properties: name: + minLength: 1 type: string sha256: + pattern: ^[a-fA-F0-9]{64}$ type: string size: format: int32 + minimum: 1 type: integer required: - name @@ -54,6 +57,7 @@ spec: type: object type: array contentHash: + pattern: ^[a-fA-F0-9]{64}$ type: string contextUIDs: items: @@ -122,6 +126,7 @@ spec: type: array revision: format: int64 + minimum: 1 type: integer servingTargets: items: @@ -139,8 +144,10 @@ spec: type: boolean writerEpoch: format: int64 + minimum: 1 type: integer zoneApex: + minLength: 1 type: string zoneRef: description: |- @@ -149,14 +156,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -174,6 +184,16 @@ spec: x-kubernetes-validations: - message: publication manifests are immutable rule: oldSelf == self + - message: zoneRef requires name and UID + rule: self.zoneRef.name.size() > 0 && has(self.zoneRef.uid) && self.zoneRef.uid.size() + > 0 + - message: previousRevision must precede revision + rule: '!has(self.previousRevision) || (self.previousRevision >= 0 && + self.previousRevision < self.revision)' + - message: withdrawal requires empty content; publication requires chunks + rule: '(has(self.tombstone) && self.tombstone) ? (self.chunks.size() + == 0 && self.contentHash == ''e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'') + : self.chunks.size() > 0' status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml index 2e9ae926..5f53fe6b 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml @@ -43,6 +43,8 @@ spec: activeManifestName: type: string holderIdentity: + maxLength: 512 + minLength: 1 type: string lastContentHash: type: string @@ -51,16 +53,23 @@ spec: type: string nextRevision: format: int64 + minimum: 1 type: integer writerEpoch: format: int64 + minimum: 1 type: integer zoneUID: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 + minLength: 1 type: string + x-kubernetes-validations: + - message: zoneUID is immutable + rule: self == oldSelf required: - holderIdentity - leaseUntil @@ -68,6 +77,13 @@ spec: - writerEpoch - zoneUID type: object + x-kubernetes-validations: + - message: publication ownership fences must not decrease + rule: self.writerEpoch >= oldSelf.writerEpoch && self.nextRevision >= + oldSelf.nextRevision + - message: ownership transfer requires a new writer epoch + rule: self.holderIdentity == oldSelf.holderIdentity || self.writerEpoch + > oldSelf.writerEpoch required: - spec type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml index adab7d82..4875ae95 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -68,6 +68,7 @@ spec: node: properties: address: + maxLength: 253 minLength: 1 type: string port: @@ -93,6 +94,7 @@ spec: regional: properties: address: + maxLength: 253 minLength: 1 type: string port: @@ -134,14 +136,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -161,8 +166,12 @@ spec: placement: properties: region: + maxLength: 253 + minLength: 1 type: string shard: + maxLength: 253 + minLength: 1 type: string required: - region @@ -180,14 +189,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -197,6 +209,7 @@ spec: UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string resolverContextRef: description: |- @@ -205,14 +218,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml index dab793c6..cd4e5287 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml @@ -50,14 +50,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -69,14 +72,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name From bc7a505804c4d1d1560f3e01bd9b074da06316f2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:15:58 -0500 Subject: [PATCH 3/4] Format publication schema validation tests --- api/v1alpha1/internaldns_service_validation_envtest_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/internaldns_service_validation_envtest_test.go b/api/v1alpha1/internaldns_service_validation_envtest_test.go index 3c1c1efd..7a356145 100644 --- a/api/v1alpha1/internaldns_service_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_service_validation_envtest_test.go @@ -4,11 +4,12 @@ package v1alpha1 import ( "context" - "k8s.io/apimachinery/pkg/types" "strings" "testing" "time" + "k8s.io/apimachinery/pkg/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" From a77ed35cb55b35960d255121f790294774cafaa6 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 11:21:42 -0500 Subject: [PATCH 4/4] refactor: Consolidate DNS continuity contracts --- api/v1alpha1/internaldns_service_types.go | 3 +++ api/v1alpha1/zz_generated.deepcopy.go | 7 +++++++ .../dns.networking.miloapis.com_dnsresolverbindings.yaml | 7 +++++++ internal/internaldns/model/model.go | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go index dc57e565..f00fc424 100644 --- a/api/v1alpha1/internaldns_service_types.go +++ b/api/v1alpha1/internaldns_service_types.go @@ -50,6 +50,9 @@ type DNSResolverBindingConfiguration struct { Listeners DNSResolverBindingListeners `json:"listeners"` // +kubebuilder:validation:MaxItems=256 ZoneRefs []DNSObjectReference `json:"zoneRefs"` + // ZoneApices reserves each private namespace before publication is available. + // +kubebuilder:validation:MaxProperties=256 + ZoneApices map[string]string `json:"zoneApices,omitempty"` } // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="listener identity is immutable for an access binding" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6a5b004c..c69e6921 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1401,6 +1401,13 @@ func (in *DNSResolverBindingConfiguration) DeepCopyInto(out *DNSResolverBindingC *out = make([]DNSObjectReference, len(*in)) copy(*out, *in) } + if in.ZoneApices != nil { + in, out := &in.ZoneApices, &out.ZoneApices + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingConfiguration. diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml index 4d78b627..81137d8c 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -124,6 +124,13 @@ spec: format: int64 minimum: 1 type: integer + zoneApices: + additionalProperties: + type: string + description: ZoneApices reserves each private namespace before + publication is available. + maxProperties: 256 + type: object zoneRefs: items: description: |- diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index 272e7cda..b6f1e5dd 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -89,6 +89,7 @@ type Backend struct { } type ZoneAttachment struct { + Pending bool `json:"pending,omitempty"` ZoneUID string `json:"zoneUID"` Apex string `json:"apex"` RequiredPublicationEpoch uint64 `json:"requiredPublicationEpoch"` @@ -111,6 +112,9 @@ type Binding struct { ClusterBackends []Backend `json:"clusterBackends"` Authorization Authorization `json:"authorization"` Tombstone bool `json:"tombstone,omitempty"` + // ConfigurationPending gates queries while attached publications are absent. + // It does not revoke or advance the source authorization. + ConfigurationPending bool `json:"configurationPending,omitempty"` } func (b Binding) ViewName() string { @@ -191,6 +195,9 @@ func (s ServingSnapshot) Validate() error { if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 { return fmt.Errorf("binding %q has incomplete transport or authorization", b.BindingUID) } + if b.ConfigurationPending && len(b.Zones) != 0 { + return fmt.Errorf("binding %q pending configuration must not expose partial zones", b.BindingUID) + } if err := validateTransports(b.Transports); err != nil { return fmt.Errorf("binding %q: %w", b.BindingUID, err) }