diff --git a/api/v1alpha1/internaldns_binding_test.go b/api/v1alpha1/internaldns_binding_test.go new file mode 100644 index 00000000..a1e4688f --- /dev/null +++ b/api/v1alpha1/internaldns_binding_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "encoding/json" + "testing" +) + +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, Query: DNSResolverBindingQuery{NodeAddress: "fd53::1", RegionalAddress: "fd54::1"}, ZoneRefs: []DNSObjectReference{{Name: "private", UID: "zone-uid"}}}, + Version: DNSResolverAccessVersion{Epoch: 3, Sequence: 4}, + }} + copy := original.DeepCopy() + copy.Spec.Configuration.ZoneRefs[0].Name = "changed" + if original.Spec.Configuration.ZoneRefs[0].Name != "private" { + 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["version"] == 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..78d44fbb --- /dev/null +++ b/api/v1alpha1/internaldns_service_types.go @@ -0,0 +1,288 @@ +// 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.version.epoch > oldSelf.version.epoch || (self.version.epoch == oldSelf.version.epoch && self.version.sequence >= oldSelf.version.sequence)",message="access versions must not decrease" +// +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.version.epoch > oldSelf.version.epoch || (self.version.epoch == oldSelf.version.epoch && self.version.sequence > oldSelf.version.sequence)",message="reactivating withdrawn access requires a new access version" +type DNSResolverBindingSpec struct { + Source DNSResolverBindingSource `json:"source"` + Placement DNSResolverBindingPlacement `json:"placement"` + Configuration DNSResolverBindingConfiguration `json:"configuration"` + Version DNSResolverAccessVersion `json:"version"` + 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 { + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MaxLength=128 + 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 { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Region string `json:"region"` + // +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" +type DNSResolverBindingConfiguration struct { + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + Query DNSResolverBindingQuery `json:"query"` + // +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="query destinations are immutable for an access binding" +type DNSResolverBindingQuery struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + NodeAddress string `json:"nodeAddress"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + RegionalAddress string `json:"regionalAddress"` +} + +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"` + AccessEpoch int64 `json:"accessEpoch,omitempty"` + AccessSequence int64 `json:"accessSequence,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 { + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` + SHA256 string `json:"sha256"` + // +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"` + // +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 { + Region string `json:"region"` + Shard string `json:"shard"` +} + +type DNSRegistrationFence struct { + UID types.UID `json:"uid"` + Generation int64 `json:"generation"` +} + +// DNSContributionFence retains desired-state generations and retired lifetimes. +type DNSContributionFence struct { + UID types.UID `json:"uid"` + Generation int64 `json:"generation"` + Sequence int64 `json:"sequence"` + Retired bool `json:"retired,omitempty"` + SourceName string `json:"sourceName,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"` + // +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 +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. +// +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 { + // +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"` + 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/internaldns_service_validation_envtest_test.go b/api/v1alpha1/internaldns_service_validation_envtest_test.go new file mode 100644 index 00000000..7a356145 --- /dev/null +++ b/api/v1alpha1/internaldns_service_validation_envtest_test.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "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" +) + +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 106a41f3..caff6b78 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -164,6 +164,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { t.Fatal("status write changed producer desired state") } }) + validateServiceDNSContracts(t, cl) validateReferenceIdentity(t, cl, requireInvalid) validateRegistrationBounds(t, cl, requireInvalid) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index df717379..e85932b3 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. @@ -108,6 +109,41 @@ 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 +} + +// 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 *DNSContributionRecordSet) DeepCopyInto(out *DNSContributionRecordSet) { *out = *in @@ -259,6 +295,322 @@ func (in *DNSObjectReference) DeepCopy() *DNSObjectReference { 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)) + copy(*out, *in) + } + 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 @@ -501,6 +853,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 @@ -768,6 +1135,188 @@ func (in *DNSResolverAccessVersion) DeepCopy() *DNSResolverAccessVersion { 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 + out.Query = in.Query + if in.ZoneRefs != nil { + in, out := &in.ZoneRefs, &out.ZoneRefs + *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. +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 *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 *DNSResolverBindingQuery) DeepCopyInto(out *DNSResolverBindingQuery) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingQuery. +func (in *DNSResolverBindingQuery) DeepCopy() *DNSResolverBindingQuery { + if in == nil { + return nil + } + out := new(DNSResolverBindingQuery) + 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) + out.Version = in.Version +} + +// 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 @@ -917,6 +1466,111 @@ func (in *DNSResolverContextStatus) DeepCopy() *DNSResolverContextStatus { 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/cmd/leader_election_test.go b/cmd/leader_election_test.go index 3c870630..ff36b10f 100644 --- a/cmd/leader_election_test.go +++ b/cmd/leader_election_test.go @@ -42,7 +42,7 @@ func TestEachRoleLeadsItsOwnLease(t *testing.T) { if err != nil { t.Fatalf("manager for %s: %v", name, err) } - go func() { _ = mgr.Start(ctx) }() + goUntilCleanup(t, func() error { return mgr.Start(ctx) }) select { case <-mgr.Elected(): case <-time.After(15 * time.Second): diff --git a/cmd/nonleader_test.go b/cmd/nonleader_test.go index f50c3532..d4cf29f0 100644 --- a/cmd/nonleader_test.go +++ b/cmd/nonleader_test.go @@ -215,7 +215,7 @@ func startReplica( t.Fatalf("setting up the replica: %v", err) } } - go func() { _ = runReplicator(ctx, runnables, downstream, mcmgr) }() + goUntilCleanup(t, func() error { return runReplicator(ctx, runnables, downstream, mcmgr) }) return mcmgr } @@ -281,6 +281,28 @@ func annotatedWithin(ctx context.Context, t *testing.T, m *dnswebhook.DNSRecordS return false } +// goUntilCleanup runs run in the background and, when the test ends, waits for +// it to return. The caller cancels run's context before cleanup. A manager +// still running when envtest stops its API server keeps watches and lease +// renewals open, and the API server then outlasts envtest's stop timeout. +// Cleanups run in reverse order, so every caller after startEnvtest is waited +// for before its API server stops. +func goUntilCleanup(t *testing.T, run func() error) { + t.Helper() + done := make(chan struct{}) + go func() { + defer close(done) + _ = run() + }() + t.Cleanup(func() { + select { + case <-done: + case <-time.After(30 * time.Second): + t.Error("a manager did not stop within 30 s of its context ending") + } + }) +} + // startEnvtest starts an API server with the CRDs in dirs and stops it when // the test ends. func startEnvtest(t *testing.T, dirs ...string) *rest.Config { 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..ef9d0efb --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml @@ -0,0 +1,79 @@ +--- +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 + minimum: 0 + 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 + 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 + - 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..609ede9f --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml @@ -0,0 +1,306 @@ +--- +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: + minLength: 1 + type: string + sha256: + pattern: ^[a-fA-F0-9]{64}$ + type: string + size: + format: int32 + minimum: 1 + type: integer + required: + - name + - sha256 + - size + type: object + type: array + contentHash: + pattern: ^[a-fA-F0-9]{64}$ + 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 retains desired-state generations + and retired lifetimes. + properties: + generation: + format: int64 + type: integer + retired: + type: boolean + sequence: + format: int64 + type: integer + sourceName: + 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: + - generation + - 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 + minimum: 1 + type: integer + servingTargets: + items: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + type: array + tombstone: + type: boolean + writerEpoch: + format: int64 + minimum: 1 + type: integer + zoneApex: + minLength: 1 + 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 + 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 + type: object + required: + - chunks + - contentHash + - contextUIDs + - generatedAt + - revision + - writerEpoch + - zoneApex + - zoneRef + type: object + 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: + 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: + accessEpoch: + format: int64 + type: integer + accessSequence: + 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..5f53fe6b --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml @@ -0,0 +1,91 @@ +--- +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: + maxLength: 512 + minLength: 1 + type: string + lastContentHash: + type: string + leaseUntil: + format: date-time + 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 + - nextRevision + - 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 + 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..ed6f0d47 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -0,0 +1,347 @@ +--- +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: + configuration: + properties: + generation: + format: int64 + minimum: 1 + type: integer + query: + properties: + nodeAddress: + maxLength: 253 + minLength: 1 + type: string + regionalAddress: + maxLength: 253 + minLength: 1 + type: string + required: + - nodeAddress + - regionalAddress + type: object + x-kubernetes-validations: + - message: query destinations are immutable for an access binding + rule: oldSelf == self + revision: + 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: |- + 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 + 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 + type: object + maxItems: 256 + type: array + required: + - generation + - query + - 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: + maxLength: 253 + minLength: 1 + type: string + shard: + maxLength: 253 + minLength: 1 + 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 + 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 + 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. + maxLength: 128 + 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 + 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 + 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 + version: + properties: + epoch: + format: int64 + minimum: 1 + type: integer + sequence: + format: int64 + minimum: 1 + type: integer + required: + - epoch + - sequence + type: object + required: + - configuration + - placement + - source + - version + 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: access versions must not decrease + rule: self.version.epoch > oldSelf.version.epoch || (self.version.epoch + == oldSelf.version.epoch && self.version.sequence >= oldSelf.version.sequence) + - 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 access version + rule: '!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) + && self.tombstone) || self.version.epoch > oldSelf.version.epoch || + (self.version.epoch == oldSelf.version.epoch && self.version.sequence + > oldSelf.version.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: + accessEpoch: + format: int64 + type: integer + accessSequence: + 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..cd4e5287 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml @@ -0,0 +1,145 @@ +--- +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 + 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 + 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 + 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 + 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 81e6435a..7e9375dd 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/config/iam/protected-resources/internal-dns.yaml b/config/iam/protected-resources/internal-dns.yaml index d39df207..a1fe8314 100644 --- a/config/iam/protected-resources/internal-dns.yaml +++ b/config/iam/protected-resources/internal-dns.yaml @@ -105,3 +105,87 @@ spec: parentResources: - apiGroup: resourcemanager.miloapis.com kind: Project +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnsresolverbinding +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSResolverBinding + plural: dnsresolverbindings + singular: dnsresolverbinding + permissions: [list, get, watch, create, update, patch, delete] + subresources: + - name: status + permissions: [get, update, patch] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnspublicationmanifest +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSPublicationManifest + plural: dnspublicationmanifests + singular: dnspublicationmanifest + permissions: [list, get, watch, create, update, patch, delete] + subresources: + - name: status + permissions: [get, update, patch] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnspublicationchunk +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSPublicationChunk + plural: dnspublicationchunks + singular: dnspublicationchunk + permissions: [list, get, watch, create, update, patch, delete] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnstransportoutbox +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSTransportOutbox + plural: dnstransportoutboxes + singular: dnstransportoutbox + permissions: [list, get, watch, create, update, patch, delete] + subresources: + - name: status + permissions: [get, update, patch] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnspublicationownership +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSPublicationOwnership + plural: dnspublicationownerships + singular: dnspublicationownership + permissions: [list, get, watch, create, update, patch, delete] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project 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/model/model.go b/internal/internaldns/model/model.go index 1f613b98..cc29882b 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -19,6 +19,8 @@ import ( const APIVersion = "internal.dns.networking.miloapis.com/v1alpha6" +var ErrUnsupportedEnvelope = errors.New("unsupported DNS envelope") + type Kind string const ( @@ -30,6 +32,11 @@ const ( // Envelope is the only value written to JetStream. Epoch and Revision are // repeated outside Payload so consumers can reject stale events before decode. +// +// The header (every field except Payload) is frozen across wire versions: a +// future APIVersion may change Payload but must keep these fields, names and +// meanings, so that older consumers can still fence and park it. Changing the +// header requires a new stream, not a new APIVersion. type Envelope struct { APIVersion string `json:"apiVersion"` Kind Kind `json:"kind"` @@ -52,20 +59,25 @@ func NewEnvelope(kind Kind, eventID, region, shard, uid string, epoch, revision return e, e.Validate() } +// Validate applies the header rules before the version gate. That is +// deliberate: the Envelope header is frozen across wire versions (see +// Envelope), so a header that breaks these rules is malformed in every +// version and is terminated, while a well-formed header with an unknown +// apiVersion or kind is reported as ErrUnsupportedEnvelope and parked. func (e Envelope) Validate() error { + if e.APIVersion == "" || e.Kind == "" || e.EventID == "" || e.Region == "" || e.Shard == "" || e.ResourceUID == "" || e.Epoch == 0 || e.Revision == 0 || e.PublishedAt.IsZero() || !json.Valid(e.Payload) { + return errors.New("envelope version, kind, identity, epoch, revision, timestamp, and JSON payload are required") + } + if SafeToken(e.Region) != e.Region || SafeToken(e.Shard) != e.Shard { + return errors.New("region and shard must be safe lowercase identifiers") + } if e.APIVersion != APIVersion { - return fmt.Errorf("unsupported apiVersion %q", e.APIVersion) + return fmt.Errorf("%w: apiVersion %q", ErrUnsupportedEnvelope, e.APIVersion) } switch e.Kind { case KindServingSnapshot, KindPublicationChunk, KindPublicationManifest, KindMemberAck: default: - return fmt.Errorf("unsupported kind %q", e.Kind) - } - if e.EventID == "" || e.Region == "" || e.Shard == "" || e.ResourceUID == "" || e.Epoch == 0 || e.Revision == 0 || e.PublishedAt.IsZero() || len(e.Payload) == 0 { - return errors.New("envelope identity, epoch, revision, timestamp, and payload are required") - } - if SafeToken(e.Region) != e.Region || SafeToken(e.Shard) != e.Shard { - return errors.New("region and shard must be safe lowercase identifiers") + return fmt.Errorf("%w: kind %q", ErrUnsupportedEnvelope, e.Kind) } return nil } @@ -89,6 +101,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 +124,9 @@ type Binding struct { ClusterBackends []Backend `json:"clusterBackends"` Version AccessVersion `json:"version"` Tombstone bool `json:"tombstone,omitempty"` + // ConfigurationPending gates queries while attached publications are absent. + // It does not revoke or advance the access version. + ConfigurationPending bool `json:"configurationPending,omitempty"` } func (b Binding) ViewName() string { @@ -191,6 +207,9 @@ func (s ServingSnapshot) Validate() error { if b.Port == 0 || len(b.Transports) == 0 || b.Version.Epoch == 0 || b.Version.Sequence == 0 { return fmt.Errorf("binding %q has incomplete transport or access version", 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) } diff --git a/internal/internaldns/model/model_test.go b/internal/internaldns/model/model_test.go index 470101be..13e30a9d 100644 --- a/internal/internaldns/model/model_test.go +++ b/internal/internaldns/model/model_test.go @@ -2,6 +2,7 @@ package model import ( "encoding/json" + "errors" "testing" "time" ) @@ -91,3 +92,18 @@ func TestEnvelopeRejectsObsoleteWireVersion(t *testing.T) { t.Fatal("obsolete transport version accepted") } } + +func TestEnvelopeHeaderIsFrozenAcrossVersions(t *testing.T) { + future, err := NewEnvelope(KindMemberAck, "event", "central", "shared", "uid", 1, 1, time.Now(), MemberAck{MemberID: "member"}) + if err != nil { + t.Fatal(err) + } + future.APIVersion = "internal.dns.networking.miloapis.com/v1beta1" + if err := future.Validate(); !errors.Is(err, ErrUnsupportedEnvelope) { + t.Fatalf("future envelope with the frozen header was not parked: %v", err) + } + future.EventID = "" + if err := future.Validate(); err == nil || errors.Is(err, ErrUnsupportedEnvelope) { + t.Fatalf("future envelope that breaks the frozen header must be malformed, got %v", err) + } +} diff --git a/internal/internaldns/transport/jetstream.go b/internal/internaldns/transport/jetstream.go new file mode 100644 index 00000000..e62d0f9d --- /dev/null +++ b/internal/internaldns/transport/jetstream.go @@ -0,0 +1,442 @@ +// 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 retry delivery while local health checks govern 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 { + if errors.Is(err, model.ErrUnsupportedEnvelope) { + _ = m.NakWithDelay(time.Minute) + } else { + _ = 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 errors.Is(err, jetstream.ErrNoKeysFound) { + // A new region's bucket is empty until its first snapshot. + return nil, nil + } + 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..aa2e9e42 --- /dev/null +++ b/internal/internaldns/transport/jetstream_test.go @@ -0,0 +1,131 @@ +package transport + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/nats-io/nats.go/jetstream" + "go.miloapis.com/dns-operator/internal/internaldns/model" +) + +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") + } +} + +type envelopeMessage struct { + jetstream.Msg + data []byte + terminated bool + retryDelay time.Duration +} + +func (m *envelopeMessage) Data() []byte { return m.data } +func (m *envelopeMessage) TermWithReason(string) error { + m.terminated = true + return nil +} +func (m *envelopeMessage) NakWithDelay(delay time.Duration) error { + m.retryDelay = delay + return nil +} + +type envelopeConsumer struct { + jetstream.Consumer + message jetstream.Msg +} + +func (c envelopeConsumer) Next(...jetstream.FetchOpt) (jetstream.Msg, error) { + return c.message, nil +} + +func TestUnsupportedEnvelopesRemainDeliverable(t *testing.T) { + for _, kind := range []model.Kind{model.KindMemberAck, "FutureKind"} { + env := model.Envelope{APIVersion: "future-version", Kind: kind, EventID: "event", Region: "east", Shard: "shared", ResourceUID: "resource", Epoch: 1, Revision: 1, PublishedAt: time.Now(), Payload: json.RawMessage(`{}`)} + if kind != model.KindMemberAck { + env.APIVersion = model.APIVersion + } + data, err := json.Marshal(env) + if err != nil { + t.Fatal(err) + } + message := &envelopeMessage{data: data} + c := Consumer{c: envelopeConsumer{message: message}} + if _, err := c.Next(context.Background()); !errors.Is(err, model.ErrUnsupportedEnvelope) || message.terminated || message.retryDelay != time.Minute { + t.Fatalf("unsupported event not retained: error=%v terminated=%t retry=%v", err, message.terminated, message.retryDelay) + } + } +} + +func TestMalformedEnvelopesAreTerminated(t *testing.T) { + for _, data := range []string{`invalid-json`, `{"apiVersion":"future","kind":"FutureKind"}`} { + message := &envelopeMessage{data: []byte(data)} + c := Consumer{c: envelopeConsumer{message: message}} + if _, err := c.Next(context.Background()); err == nil || !message.terminated || message.retryDelay != 0 { + t.Fatalf("malformed event not terminated: error=%v terminated=%t retry=%v", err, message.terminated, message.retryDelay) + } + } +} + +type emptyKeyValue struct{ jetstream.KeyValue } + +func (emptyKeyValue) Keys(context.Context, ...jetstream.WatchOpt) ([]string, error) { + return nil, jetstream.ErrNoKeysFound +} + +func TestSnapshotListOfEmptyBucketIsEmpty(t *testing.T) { + envelopes, err := (&SnapshotStore{kv: emptyKeyValue{}}).List(context.Background(), "central.") + if err != nil || len(envelopes) != 0 { + t.Fatalf("empty bucket must list no snapshots, got %v, %v", envelopes, err) + } +}