From 4c81fecc5aa21d8b454949e8c88dbf2151c2e22c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 19:01:47 -0500 Subject: [PATCH 01/28] feat: Define private DNS access and publication APIs --- api/v1alpha1/internaldns_access_test.go | 42 + api/v1alpha1/internaldns_project_types.go | 381 ++++++ api/v1alpha1/internaldns_types_test.go | 33 + api/v1alpha1/zz_generated.deepcopy.go | 1071 ++++++++++++++++- ...ng.miloapis.com_dnscontributiongrants.yaml | 175 +++ ...ing.miloapis.com_dnsmanagednamespaces.yaml | 184 +++ ...orking.miloapis.com_dnsnamingpolicies.yaml | 320 +++++ ...g.miloapis.com_dnsrecordcontributions.yaml | 411 +++++++ ...working.miloapis.com_dnsregistrations.yaml | 176 +++ ...iloapis.com_dnsresolveraccessbindings.yaml | 194 +++ ...king.miloapis.com_dnsresolvercontexts.yaml | 161 +++ ...king.miloapis.com_dnszoneassociations.yaml | 262 ++++ config/crd/kustomization.yaml | 16 + config/internal-dns/examples/context.yaml | 11 + internal/internaldns/admission/handler.go | 522 ++++++++ .../internaldns/admission/handler_test.go | 262 ++++ internal/internaldns/model/model.go | 547 +++++++++ internal/internaldns/model/model_test.go | 93 ++ internal/internaldns/model/private_records.go | 22 + internal/internaldns/model/snapshot_keys.go | 45 + internal/internaldns/model/subjects.go | 23 + 21 files changed, 4923 insertions(+), 28 deletions(-) create mode 100644 api/v1alpha1/internaldns_access_test.go create mode 100644 api/v1alpha1/internaldns_project_types.go create mode 100644 api/v1alpha1/internaldns_types_test.go create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml create mode 100644 config/internal-dns/examples/context.yaml create mode 100644 internal/internaldns/admission/handler.go create mode 100644 internal/internaldns/admission/handler_test.go create mode 100644 internal/internaldns/model/model.go create mode 100644 internal/internaldns/model/model_test.go create mode 100644 internal/internaldns/model/private_records.go create mode 100644 internal/internaldns/model/snapshot_keys.go create mode 100644 internal/internaldns/model/subjects.go diff --git a/api/v1alpha1/internaldns_access_test.go b/api/v1alpha1/internaldns_access_test.go new file mode 100644 index 00000000..d4abda23 --- /dev/null +++ b/api/v1alpha1/internaldns_access_test.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "encoding/json" + "testing" + "time" +) + +func TestAccessAuthorizationPreservesDeadlinePrecision(t *testing.T) { + const raw = `{"writerEpoch":1,"sequence":2,"validUntil":"2026-10-08T23:22:29.022357123Z"}` + var authorization DNSResolverAccessAuthorization + if err := json.Unmarshal([]byte(raw), &authorization); err != nil { + t.Fatal(err) + } + for i := 0; i < 3; i++ { + encoded, err := json.Marshal(authorization) + if err != nil { + t.Fatal(err) + } + if string(encoded) != raw { + t.Fatalf("authorization deadline changed on round trip: %s", encoded) + } + if err := json.Unmarshal(encoded, &authorization); err != nil { + t.Fatal(err) + } + } + if authorization.ValidUntil.Nanosecond() != 22357123 { + t.Fatal("authorization lost nanosecond precision") + } + // The same instant in another time zone serializes to the same UTC fence. + authorization.ValidUntil.Time = authorization.ValidUntil.In(time.FixedZone("offset", 3600)) + encoded, err := json.Marshal(authorization) + if err != nil || string(encoded) != raw { + t.Fatalf("UTC normalization changed authorization: %s, %v", encoded, err) + } + encoded, err = json.Marshal(DNSResolverAccessAuthorization{}) + if err != nil || string(encoded) != `{"writerEpoch":0,"sequence":0,"validUntil":null}` { + t.Fatalf("zero value encoding changed field names or null deadline: %s, %v", encoded, err) + } +} diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go new file mode 100644 index 00000000..ddc481df --- /dev/null +++ b/api/v1alpha1/internaldns_project_types.go @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "encoding/json" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// DNSObjectReference pins a namespaced object by API-assigned UID. Generation +// is used where a writer must also be fenced against policy changes. +type DNSObjectReference struct { + Name string `json:"name"` + UID types.UID `json:"uid,omitempty"` + Generation int64 `json:"generation,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" +// +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" +// +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" +type DNSZoneAssociationSpec struct { + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + VPCRef DNSObjectReference `json:"vpcRef,omitempty"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef,omitempty"` + Managed bool `json:"managed,omitempty"` +} + +type DNSZoneAssociationStatus struct { + ResolvedDNSZoneRef DNSObjectReference `json:"resolvedDNSZoneRef,omitempty"` + ResolvedVPCRef DNSObjectReference `json:"resolvedVPCRef,omitempty"` + ResolvedResolverContextRef DNSObjectReference `json:"resolvedResolverContextRef,omitempty"` + BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// DNSResolverContext is the DNS-owned serving and namespace boundary created by +// a trusted network integration. consumerID is opaque to DNS and immutable. +// +kubebuilder:validation:XValidation:rule="oldSelf.consumerID == self.consumerID",message="consumerID is immutable" +type DNSResolverContextSpec struct { + ConsumerID string `json:"consumerID"` + ManagedNamespace DNSResolverContextManagedNamespace `json:"managedNamespace,omitempty"` +} + +type DNSResolverContextManagedNamespace struct { + Enabled bool `json:"enabled,omitempty"` +} + +type DNSResolverContextManagedNamespaceStatus struct { + DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + Suffix string `json:"suffix,omitempty"` +} + +type DNSResolverContextStatus struct { + ManagedNamespace DNSResolverContextManagedNamespaceStatus `json:"managedNamespace,omitempty"` + AccessWriterEpoch int64 `json:"accessWriterEpoch,omitempty"` + ServingTargets []DNSResolverContextServingTarget `json:"servingTargets,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type DNSResolverContextServingTarget struct { + Region string `json:"region"` + Shard string `json:"shard"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSResolverContext struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSResolverContextSpec `json:"spec"` + Status DNSResolverContextStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSResolverContextList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSResolverContext `json:"items"` +} + +// +kubebuilder:validation:Enum=DestinationAddress +type DNSResolverQueryIdentityType string + +const DNSResolverQueryIdentityDestinationAddress DNSResolverQueryIdentityType = "DestinationAddress" + +type DNSResolverQueryIdentity struct { + Type DNSResolverQueryIdentityType `json:"type"` + Value string `json:"value"` +} + +type DNSResolverAccessAuthorization struct { + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 + Sequence int64 `json:"sequence"` + ValidUntil metav1.Time `json:"validUntil"` +} + +// MarshalJSON preserves the source authorization deadline exactly. metav1.Time +// accepts fractional seconds on input but its default JSON encoder drops them, +// which would mutate a same-fence deadline when projecting an access binding. +func (a DNSResolverAccessAuthorization) MarshalJSON() ([]byte, error) { + var deadline *time.Time + if !a.ValidUntil.IsZero() { + instant := a.ValidUntil.UTC() + deadline = &instant + } + return json.Marshal(struct { + WriterEpoch int64 `json:"writerEpoch"` + Sequence int64 `json:"sequence"` + ValidUntil *time.Time `json:"validUntil"` + }{WriterEpoch: a.WriterEpoch, Sequence: a.Sequence, ValidUntil: deadline}) +} + +type DNSResolverAccessBindingSpec struct { + ContextRef DNSObjectReference `json:"contextRef"` + Region string `json:"region"` + QueryIdentity DNSResolverQueryIdentity `json:"queryIdentity"` + Port int32 `json:"port"` + Transports []string `json:"transports"` + Authorization DNSResolverAccessAuthorization `json:"authorization"` +} + +type DNSResolverAccessBindingStatus struct { + ObservedSequence int64 `json:"observedSequence,omitempty"` + BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSResolverAccessBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSResolverAccessBindingSpec `json:"spec"` + Status DNSResolverAccessBindingStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSResolverAccessBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSResolverAccessBinding `json:"items"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSZoneAssociation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSZoneAssociationSpec `json:"spec"` + Status DNSZoneAssociationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSZoneAssociationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSZoneAssociation `json:"items"` +} + +// +kubebuilder:validation:Enum=EligibleContributions;Persistent +type DNSPublicationPolicy string + +const ( + DNSPublicationPolicyEligibleContributions DNSPublicationPolicy = "EligibleContributions" + DNSPublicationPolicyPersistent DNSPublicationPolicy = "Persistent" +) + +type DNSRegistrationSpec struct { + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + Name string `json:"name"` + RecordTypes []RRType `json:"recordTypes"` + PublicationPolicy DNSPublicationPolicy `json:"publicationPolicy"` + TTLSeconds int32 `json:"ttlSeconds"` + ReservedDescendants []string `json:"reservedDescendants,omitempty"` +} + +type DNSRegistrationStatus struct { + CanonicalFQDN string `json:"canonicalFQDN,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + PublicationWriterEpoch int64 `json:"publicationWriterEpoch,omitempty"` + PublicationRevision int64 `json:"publicationRevision,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSRegistration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSRegistrationSpec `json:"spec"` + Status DNSRegistrationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSRegistrationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSRegistration `json:"items"` +} + +type DNSProducerPrincipal struct { + ClusterUID string `json:"clusterUID"` + Subject string `json:"subject"` +} + +type DNSContributionGrantSpec struct { + RegistrationRef DNSObjectReference `json:"registrationRef"` + ProducerID string `json:"producerID"` + Principal DNSProducerPrincipal `json:"principal"` + RecordTypes []RRType `json:"recordTypes"` + NameScopes []string `json:"nameScopes,omitempty"` +} + +type DNSContributionGrantStatus struct { + ActiveWriterEpoch int64 `json:"activeWriterEpoch,omitempty"` + ObservedGrantGeneration int64 `json:"observedGrantGeneration,omitempty"` + ObservedRegistrationGeneration int64 `json:"observedRegistrationGeneration,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSContributionGrant struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSContributionGrantSpec `json:"spec"` + Status DNSContributionGrantStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSContributionGrantList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSContributionGrant `json:"items"` +} + +type DNSContributionRecordSet struct { + RecordType RRType `json:"recordType"` + Records []RecordEntry `json:"records"` +} + +type DNSRecordContributionSpec struct { + RegistrationRef DNSObjectReference `json:"registrationRef"` + GrantRef DNSObjectReference `json:"grantRef"` + RecordSets []DNSContributionRecordSet `json:"recordSets"` +} + +type DNSRecordContributionStatus struct { + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + WriterEpoch int64 `json:"writerEpoch,omitempty"` + Sequence int64 `json:"sequence,omitempty"` + Eligible bool `json:"eligible,omitempty"` + Reason string `json:"reason,omitempty"` + ValidUntil *metav1.Time `json:"validUntil,omitempty"` + PublishedRevision int64 `json:"publishedRevision,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" +type DNSRecordContribution struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSRecordContributionSpec `json:"spec"` + Status DNSRecordContributionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSRecordContributionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSRecordContribution `json:"items"` +} + +// +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" +// +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" +// +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" +type DNSNamingPolicySpec struct { + VPCRef DNSObjectReference `json:"vpcRef,omitempty"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef,omitempty"` + AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` + // Legacy single-rule fields are retained during the v1alpha1 transition. + DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + Product string `json:"product,omitempty"` + ScopePrefix string `json:"scopePrefix,omitempty"` + Priority int32 `json:"priority,omitempty"` +} + +// +kubebuilder:validation:Enum=InstanceIdentity;ServiceDiscovery;ServiceVIP;ServiceExport +type DNSRegistrationClass string + +const ( + DNSRegistrationClassInstanceIdentity DNSRegistrationClass = "InstanceIdentity" + DNSRegistrationClassServiceDiscovery DNSRegistrationClass = "ServiceDiscovery" + DNSRegistrationClassServiceVIP DNSRegistrationClass = "ServiceVIP" + DNSRegistrationClassServiceExport DNSRegistrationClass = "ServiceExport" +) + +type DNSAdditionalNameRule struct { + RegistrationClass DNSRegistrationClass `json:"registrationClass"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + NamePrefix string `json:"namePrefix"` +} + +type DNSNamingPolicyStatus struct { + ResolvedVPCRef DNSObjectReference `json:"resolvedVPCRef,omitempty"` + ResolvedResolverContextRef DNSObjectReference `json:"resolvedResolverContextRef,omitempty"` + ResolvedDNSZoneRef DNSObjectReference `json:"resolvedDNSZoneRef,omitempty"` + ResolvedAdditionalNames []DNSResolvedAdditionalNameRule `json:"resolvedAdditionalNames,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type DNSResolvedAdditionalNameRule struct { + RegistrationClass DNSRegistrationClass `json:"registrationClass"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + NamePrefix string `json:"namePrefix"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSNamingPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSNamingPolicySpec `json:"spec"` + Status DNSNamingPolicyStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSNamingPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSNamingPolicy `json:"items"` +} + +// DNSManagedNamespace is the DNS-owned lookup and lifecycle record for the +// canonical private namespace of a VPC. Product controllers read its status; +// only the DNS platform controller writes the object. +type DNSManagedNamespaceSpec struct { + ProjectUID types.UID `json:"projectUID"` + VPCRef DNSObjectReference `json:"vpcRef"` + DomainSuffix string `json:"domainSuffix"` + DNSZoneClassName string `json:"dnsZoneClassName"` +} + +type DNSManagedNamespaceStatus struct { + DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + AssociationRef DNSObjectReference `json:"associationRef,omitempty"` + CanonicalSuffix string `json:"canonicalSuffix,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSManagedNamespace struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSManagedNamespaceSpec `json:"spec"` + Status DNSManagedNamespaceStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSManagedNamespaceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSManagedNamespace `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSContributionGrant{}, &DNSContributionGrantList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}, &DNSManagedNamespace{}, &DNSManagedNamespaceList{}) +} diff --git a/api/v1alpha1/internaldns_types_test.go b/api/v1alpha1/internaldns_types_test.go new file mode 100644 index 00000000..0929a7fc --- /dev/null +++ b/api/v1alpha1/internaldns_types_test.go @@ -0,0 +1,33 @@ +package v1alpha1 + +import ( + "encoding/json" + "testing" +) + +// Value-typed references with omitempty encode their unselected peer as {}. +// The CRD CEL deliberately tests a complete name/UID pin, rather than mere +// property presence, so this normal Go client representation remains valid. +func TestConsumerSelectorJSONUsesEmptyUnselectedReference(t *testing.T) { + for name, value := range map[string]any{ + "association context": DNSZoneAssociationSpec{ResolverContextRef: DNSObjectReference{Name: "ctx", UID: "ctx-uid"}}, + "naming legacy": DNSNamingPolicySpec{VPCRef: DNSObjectReference{Name: "vpc", UID: "vpc-uid"}}, + } { + t.Run(name, func(t *testing.T) { + data, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var object map[string]any + if err := json.Unmarshal(data, &object); err != nil { + t.Fatal(err) + } + if _, hasVPC := object["vpcRef"]; !hasVPC { + t.Fatal("expected value-typed vpcRef property") + } + if _, hasContext := object["resolverContextRef"]; !hasContext { + t.Fatal("expected value-typed resolverContextRef property") + } + }) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c86213e2..9abcf217 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -87,6 +87,502 @@ func (in *CNAMERecordSpec) DeepCopy() *CNAMERecordSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSAdditionalNameRule) DeepCopyInto(out *DNSAdditionalNameRule) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSAdditionalNameRule. +func (in *DNSAdditionalNameRule) DeepCopy() *DNSAdditionalNameRule { + if in == nil { + return nil + } + out := new(DNSAdditionalNameRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSContributionGrant) DeepCopyInto(out *DNSContributionGrant) { + *out = *in + 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 DNSContributionGrant. +func (in *DNSContributionGrant) DeepCopy() *DNSContributionGrant { + if in == nil { + return nil + } + out := new(DNSContributionGrant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSContributionGrant) 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 *DNSContributionGrantList) DeepCopyInto(out *DNSContributionGrantList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSContributionGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionGrantList. +func (in *DNSContributionGrantList) DeepCopy() *DNSContributionGrantList { + if in == nil { + return nil + } + out := new(DNSContributionGrantList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSContributionGrantList) 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 *DNSContributionGrantSpec) DeepCopyInto(out *DNSContributionGrantSpec) { + *out = *in + out.RegistrationRef = in.RegistrationRef + out.Principal = in.Principal + if in.RecordTypes != nil { + in, out := &in.RecordTypes, &out.RecordTypes + *out = make([]RRType, len(*in)) + copy(*out, *in) + } + if in.NameScopes != nil { + in, out := &in.NameScopes, &out.NameScopes + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionGrantSpec. +func (in *DNSContributionGrantSpec) DeepCopy() *DNSContributionGrantSpec { + if in == nil { + return nil + } + out := new(DNSContributionGrantSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSContributionGrantStatus) DeepCopyInto(out *DNSContributionGrantStatus) { + *out = *in + 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 DNSContributionGrantStatus. +func (in *DNSContributionGrantStatus) DeepCopy() *DNSContributionGrantStatus { + if in == nil { + return nil + } + out := new(DNSContributionGrantStatus) + 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 + if in.Records != nil { + in, out := &in.Records, &out.Records + *out = make([]RecordEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionRecordSet. +func (in *DNSContributionRecordSet) DeepCopy() *DNSContributionRecordSet { + if in == nil { + return nil + } + out := new(DNSContributionRecordSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSManagedNamespace) DeepCopyInto(out *DNSManagedNamespace) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespace. +func (in *DNSManagedNamespace) DeepCopy() *DNSManagedNamespace { + if in == nil { + return nil + } + out := new(DNSManagedNamespace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSManagedNamespace) 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 *DNSManagedNamespaceList) DeepCopyInto(out *DNSManagedNamespaceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSManagedNamespace, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceList. +func (in *DNSManagedNamespaceList) DeepCopy() *DNSManagedNamespaceList { + if in == nil { + return nil + } + out := new(DNSManagedNamespaceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSManagedNamespaceList) 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 *DNSManagedNamespaceSpec) DeepCopyInto(out *DNSManagedNamespaceSpec) { + *out = *in + out.VPCRef = in.VPCRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceSpec. +func (in *DNSManagedNamespaceSpec) DeepCopy() *DNSManagedNamespaceSpec { + if in == nil { + return nil + } + out := new(DNSManagedNamespaceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSManagedNamespaceStatus) DeepCopyInto(out *DNSManagedNamespaceStatus) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef + out.AssociationRef = in.AssociationRef + 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 DNSManagedNamespaceStatus. +func (in *DNSManagedNamespaceStatus) DeepCopy() *DNSManagedNamespaceStatus { + if in == nil { + return nil + } + out := new(DNSManagedNamespaceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSNamingPolicy) DeepCopyInto(out *DNSNamingPolicy) { + *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 DNSNamingPolicy. +func (in *DNSNamingPolicy) DeepCopy() *DNSNamingPolicy { + if in == nil { + return nil + } + out := new(DNSNamingPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSNamingPolicy) 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 *DNSNamingPolicyList) DeepCopyInto(out *DNSNamingPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSNamingPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSNamingPolicyList. +func (in *DNSNamingPolicyList) DeepCopy() *DNSNamingPolicyList { + if in == nil { + return nil + } + out := new(DNSNamingPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSNamingPolicyList) 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 *DNSNamingPolicySpec) DeepCopyInto(out *DNSNamingPolicySpec) { + *out = *in + out.VPCRef = in.VPCRef + out.ResolverContextRef = in.ResolverContextRef + if in.AdditionalNames != nil { + in, out := &in.AdditionalNames, &out.AdditionalNames + *out = make([]DNSAdditionalNameRule, len(*in)) + copy(*out, *in) + } + out.DNSZoneRef = in.DNSZoneRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSNamingPolicySpec. +func (in *DNSNamingPolicySpec) DeepCopy() *DNSNamingPolicySpec { + if in == nil { + return nil + } + out := new(DNSNamingPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSNamingPolicyStatus) DeepCopyInto(out *DNSNamingPolicyStatus) { + *out = *in + out.ResolvedVPCRef = in.ResolvedVPCRef + out.ResolvedResolverContextRef = in.ResolvedResolverContextRef + out.ResolvedDNSZoneRef = in.ResolvedDNSZoneRef + if in.ResolvedAdditionalNames != nil { + in, out := &in.ResolvedAdditionalNames, &out.ResolvedAdditionalNames + *out = make([]DNSResolvedAdditionalNameRule, len(*in)) + copy(*out, *in) + } + 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 DNSNamingPolicyStatus. +func (in *DNSNamingPolicyStatus) DeepCopy() *DNSNamingPolicyStatus { + if in == nil { + return nil + } + out := new(DNSNamingPolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSObjectReference) DeepCopyInto(out *DNSObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSObjectReference. +func (in *DNSObjectReference) DeepCopy() *DNSObjectReference { + if in == nil { + return nil + } + out := new(DNSObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSProducerPrincipal) DeepCopyInto(out *DNSProducerPrincipal) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSProducerPrincipal. +func (in *DNSProducerPrincipal) DeepCopy() *DNSProducerPrincipal { + if in == nil { + return nil + } + out := new(DNSProducerPrincipal) + 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 + 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 DNSRecordContribution. +func (in *DNSRecordContribution) DeepCopy() *DNSRecordContribution { + if in == nil { + return nil + } + out := new(DNSRecordContribution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordContribution) 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 *DNSRecordContributionList) DeepCopyInto(out *DNSRecordContributionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRecordContribution, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordContributionList. +func (in *DNSRecordContributionList) DeepCopy() *DNSRecordContributionList { + if in == nil { + return nil + } + out := new(DNSRecordContributionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordContributionList) 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 *DNSRecordContributionSpec) DeepCopyInto(out *DNSRecordContributionSpec) { + *out = *in + out.RegistrationRef = in.RegistrationRef + out.GrantRef = in.GrantRef + if in.RecordSets != nil { + in, out := &in.RecordSets, &out.RecordSets + *out = make([]DNSContributionRecordSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordContributionSpec. +func (in *DNSRecordContributionSpec) DeepCopy() *DNSRecordContributionSpec { + if in == nil { + return nil + } + out := new(DNSRecordContributionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordContributionStatus) DeepCopyInto(out *DNSRecordContributionStatus) { + *out = *in + if in.ValidUntil != nil { + in, out := &in.ValidUntil, &out.ValidUntil + *out = (*in).DeepCopy() + } + 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 DNSRecordContributionStatus. +func (in *DNSRecordContributionStatus) DeepCopy() *DNSRecordContributionStatus { + if in == nil { + return nil + } + out := new(DNSRecordContributionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordSet) DeepCopyInto(out *DNSRecordSet) { *out = *in @@ -96,18 +592,268 @@ func (in *DNSRecordSet) DeepCopyInto(out *DNSRecordSet) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSet. -func (in *DNSRecordSet) DeepCopy() *DNSRecordSet { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSet. +func (in *DNSRecordSet) DeepCopy() *DNSRecordSet { + if in == nil { + return nil + } + out := new(DNSRecordSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordSet) 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 *DNSRecordSetList) DeepCopyInto(out *DNSRecordSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRecordSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetList. +func (in *DNSRecordSetList) DeepCopy() *DNSRecordSetList { + if in == nil { + return nil + } + out := new(DNSRecordSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordSetList) 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 *DNSRecordSetSpec) DeepCopyInto(out *DNSRecordSetSpec) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef + if in.Records != nil { + in, out := &in.Records, &out.Records + *out = make([]RecordEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetSpec. +func (in *DNSRecordSetSpec) DeepCopy() *DNSRecordSetSpec { + if in == nil { + return nil + } + out := new(DNSRecordSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordSetStatus) DeepCopyInto(out *DNSRecordSetStatus) { + *out = *in + 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]) + } + } + if in.RecordSets != nil { + in, out := &in.RecordSets, &out.RecordSets + *out = make([]RecordSetStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetStatus. +func (in *DNSRecordSetStatus) DeepCopy() *DNSRecordSetStatus { + if in == nil { + return nil + } + out := new(DNSRecordSetStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistration) DeepCopyInto(out *DNSRegistration) { + *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 DNSRegistration. +func (in *DNSRegistration) DeepCopy() *DNSRegistration { + if in == nil { + return nil + } + out := new(DNSRegistration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRegistration) 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 *DNSRegistrationList) DeepCopyInto(out *DNSRegistrationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRegistration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationList. +func (in *DNSRegistrationList) DeepCopy() *DNSRegistrationList { + if in == nil { + return nil + } + out := new(DNSRegistrationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRegistrationList) 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 *DNSRegistrationSpec) DeepCopyInto(out *DNSRegistrationSpec) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef + if in.RecordTypes != nil { + in, out := &in.RecordTypes, &out.RecordTypes + *out = make([]RRType, len(*in)) + copy(*out, *in) + } + if in.ReservedDescendants != nil { + in, out := &in.ReservedDescendants, &out.ReservedDescendants + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationSpec. +func (in *DNSRegistrationSpec) DeepCopy() *DNSRegistrationSpec { + if in == nil { + return nil + } + out := new(DNSRegistrationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistrationStatus) DeepCopyInto(out *DNSRegistrationStatus) { + *out = *in + 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 DNSRegistrationStatus. +func (in *DNSRegistrationStatus) DeepCopy() *DNSRegistrationStatus { + if in == nil { + return nil + } + out := new(DNSRegistrationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolvedAdditionalNameRule) DeepCopyInto(out *DNSResolvedAdditionalNameRule) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolvedAdditionalNameRule. +func (in *DNSResolvedAdditionalNameRule) DeepCopy() *DNSResolvedAdditionalNameRule { + if in == nil { + return nil + } + out := new(DNSResolvedAdditionalNameRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverAccessAuthorization) DeepCopyInto(out *DNSResolverAccessAuthorization) { + *out = *in + in.ValidUntil.DeepCopyInto(&out.ValidUntil) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessAuthorization. +func (in *DNSResolverAccessAuthorization) DeepCopy() *DNSResolverAccessAuthorization { + if in == nil { + return nil + } + out := new(DNSResolverAccessAuthorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverAccessBinding) DeepCopyInto(out *DNSResolverAccessBinding) { + *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 DNSResolverAccessBinding. +func (in *DNSResolverAccessBinding) DeepCopy() *DNSResolverAccessBinding { if in == nil { return nil } - out := new(DNSRecordSet) + out := new(DNSResolverAccessBinding) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSRecordSet) DeepCopyObject() runtime.Object { +func (in *DNSResolverAccessBinding) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -115,31 +861,31 @@ func (in *DNSRecordSet) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSRecordSetList) DeepCopyInto(out *DNSRecordSetList) { +func (in *DNSResolverAccessBindingList) DeepCopyInto(out *DNSResolverAccessBindingList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]DNSRecordSet, len(*in)) + *out = make([]DNSResolverAccessBinding, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetList. -func (in *DNSRecordSetList) DeepCopy() *DNSRecordSetList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingList. +func (in *DNSResolverAccessBindingList) DeepCopy() *DNSResolverAccessBindingList { if in == nil { return nil } - out := new(DNSRecordSetList) + out := new(DNSResolverAccessBindingList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSRecordSetList) DeepCopyObject() runtime.Object { +func (in *DNSResolverAccessBindingList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -147,31 +893,36 @@ func (in *DNSRecordSetList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSRecordSetSpec) DeepCopyInto(out *DNSRecordSetSpec) { +func (in *DNSResolverAccessBindingSpec) DeepCopyInto(out *DNSResolverAccessBindingSpec) { *out = *in - out.DNSZoneRef = in.DNSZoneRef - if in.Records != nil { - in, out := &in.Records, &out.Records - *out = make([]RecordEntry, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + out.ContextRef = in.ContextRef + out.QueryIdentity = in.QueryIdentity + if in.Transports != nil { + in, out := &in.Transports, &out.Transports + *out = make([]string, len(*in)) + copy(*out, *in) } + in.Authorization.DeepCopyInto(&out.Authorization) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetSpec. -func (in *DNSRecordSetSpec) DeepCopy() *DNSRecordSetSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingSpec. +func (in *DNSResolverAccessBindingSpec) DeepCopy() *DNSResolverAccessBindingSpec { if in == nil { return nil } - out := new(DNSRecordSetSpec) + out := new(DNSResolverAccessBindingSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSRecordSetStatus) DeepCopyInto(out *DNSRecordSetStatus) { +func (in *DNSResolverAccessBindingStatus) DeepCopyInto(out *DNSResolverAccessBindingStatus) { *out = *in + if in.BindingRef != nil { + in, out := &in.BindingRef, &out.BindingRef + *out = new(DNSObjectReference) + **out = **in + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -179,21 +930,178 @@ func (in *DNSRecordSetStatus) DeepCopyInto(out *DNSRecordSetStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.RecordSets != nil { - in, out := &in.RecordSets, &out.RecordSets - *out = make([]RecordSetStatus, len(*in)) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingStatus. +func (in *DNSResolverAccessBindingStatus) DeepCopy() *DNSResolverAccessBindingStatus { + if in == nil { + return nil + } + out := new(DNSResolverAccessBindingStatus) + 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 + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContext. +func (in *DNSResolverContext) DeepCopy() *DNSResolverContext { + if in == nil { + return nil + } + out := new(DNSResolverContext) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverContext) 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 *DNSResolverContextList) DeepCopyInto(out *DNSResolverContextList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSResolverContext, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSetStatus. -func (in *DNSRecordSetStatus) DeepCopy() *DNSRecordSetStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContextList. +func (in *DNSResolverContextList) DeepCopy() *DNSResolverContextList { if in == nil { return nil } - out := new(DNSRecordSetStatus) + out := new(DNSResolverContextList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverContextList) 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 *DNSResolverContextManagedNamespace) DeepCopyInto(out *DNSResolverContextManagedNamespace) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContextManagedNamespace. +func (in *DNSResolverContextManagedNamespace) DeepCopy() *DNSResolverContextManagedNamespace { + if in == nil { + return nil + } + out := new(DNSResolverContextManagedNamespace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverContextManagedNamespaceStatus) DeepCopyInto(out *DNSResolverContextManagedNamespaceStatus) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContextManagedNamespaceStatus. +func (in *DNSResolverContextManagedNamespaceStatus) DeepCopy() *DNSResolverContextManagedNamespaceStatus { + if in == nil { + return nil + } + out := new(DNSResolverContextManagedNamespaceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverContextServingTarget) DeepCopyInto(out *DNSResolverContextServingTarget) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContextServingTarget. +func (in *DNSResolverContextServingTarget) DeepCopy() *DNSResolverContextServingTarget { + if in == nil { + return nil + } + out := new(DNSResolverContextServingTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverContextSpec) DeepCopyInto(out *DNSResolverContextSpec) { + *out = *in + out.ManagedNamespace = in.ManagedNamespace +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverContextSpec. +func (in *DNSResolverContextSpec) DeepCopy() *DNSResolverContextSpec { + if in == nil { + return nil + } + out := new(DNSResolverContextSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverContextStatus) DeepCopyInto(out *DNSResolverContextStatus) { + *out = *in + out.ManagedNamespace = in.ManagedNamespace + if in.ServingTargets != nil { + in, out := &in.ServingTargets, &out.ServingTargets + *out = make([]DNSResolverContextServingTarget, len(*in)) + copy(*out, *in) + } + 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 DNSResolverContextStatus. +func (in *DNSResolverContextStatus) DeepCopy() *DNSResolverContextStatus { + if in == nil { + return nil + } + out := new(DNSResolverContextStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverQueryIdentity) DeepCopyInto(out *DNSResolverQueryIdentity) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverQueryIdentity. +func (in *DNSResolverQueryIdentity) DeepCopy() *DNSResolverQueryIdentity { + if in == nil { + return nil + } + out := new(DNSResolverQueryIdentity) in.DeepCopyInto(out) return out } @@ -225,6 +1133,113 @@ func (in *DNSZone) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneAssociation) DeepCopyInto(out *DNSZoneAssociation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneAssociation. +func (in *DNSZoneAssociation) DeepCopy() *DNSZoneAssociation { + if in == nil { + return nil + } + out := new(DNSZoneAssociation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZoneAssociation) 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 *DNSZoneAssociationList) DeepCopyInto(out *DNSZoneAssociationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSZoneAssociation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneAssociationList. +func (in *DNSZoneAssociationList) DeepCopy() *DNSZoneAssociationList { + if in == nil { + return nil + } + out := new(DNSZoneAssociationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZoneAssociationList) 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 *DNSZoneAssociationSpec) DeepCopyInto(out *DNSZoneAssociationSpec) { + *out = *in + out.DNSZoneRef = in.DNSZoneRef + out.VPCRef = in.VPCRef + out.ResolverContextRef = in.ResolverContextRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneAssociationSpec. +func (in *DNSZoneAssociationSpec) DeepCopy() *DNSZoneAssociationSpec { + if in == nil { + return nil + } + out := new(DNSZoneAssociationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneAssociationStatus) DeepCopyInto(out *DNSZoneAssociationStatus) { + *out = *in + out.ResolvedDNSZoneRef = in.ResolvedDNSZoneRef + out.ResolvedVPCRef = in.ResolvedVPCRef + out.ResolvedResolverContextRef = in.ResolvedResolverContextRef + if in.BindingRef != nil { + in, out := &in.BindingRef, &out.BindingRef + *out = new(DNSObjectReference) + **out = **in + } + 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 DNSZoneAssociationStatus. +func (in *DNSZoneAssociationStatus) DeepCopy() *DNSZoneAssociationStatus { + if in == nil { + return nil + } + out := new(DNSZoneAssociationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSZoneClass) DeepCopyInto(out *DNSZoneClass) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml new file mode 100644 index 00000000..d33fde46 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml @@ -0,0 +1,175 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnscontributiongrants.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSContributionGrant + listKind: DNSContributionGrantList + plural: dnscontributiongrants + singular: dnscontributiongrant + 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: + nameScopes: + items: + type: string + type: array + principal: + properties: + clusterUID: + type: string + subject: + type: string + required: + - clusterUID + - subject + type: object + producerID: + type: string + recordTypes: + items: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + type: array + registrationRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - principal + - producerID + - recordTypes + - registrationRef + type: object + status: + properties: + activeWriterEpoch: + format: int64 + type: integer + 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 + observedGrantGeneration: + format: int64 + type: integer + observedRegistrationGeneration: + 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_dnsmanagednamespaces.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml new file mode 100644 index 00000000..9e56dcaf --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml @@ -0,0 +1,184 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsmanagednamespaces.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSManagedNamespace + listKind: DNSManagedNamespaceList + plural: dnsmanagednamespaces + singular: dnsmanagednamespace + 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: |- + DNSManagedNamespace is the DNS-owned lookup and lifecycle record for the + canonical private namespace of a VPC. Product controllers read its status; + only the DNS platform controller writes the object. + properties: + dnsZoneClassName: + type: string + domainSuffix: + type: string + projectUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + vpcRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - dnsZoneClassName + - domainSuffix + - projectUID + - vpcRef + type: object + status: + properties: + associationRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + canonicalSuffix: + type: string + 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 + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml new file mode 100644 index 00000000..0c9f59ed --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -0,0 +1,320 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsnamingpolicies.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSNamingPolicy + listKind: DNSNamingPolicyList + plural: dnsnamingpolicies + singular: dnsnamingpolicy + 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: + additionalNames: + items: + properties: + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + namePrefix: + type: string + registrationClass: + enum: + - InstanceIdentity + - ServiceDiscovery + - ServiceVIP + - ServiceExport + type: string + required: + - dnsZoneRef + - namePrefix + - registrationClass + type: object + type: array + dnsZoneRef: + description: Legacy single-rule fields are retained during the v1alpha1 + transition. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + priority: + format: int32 + type: integer + product: + type: string + resolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + scopePrefix: + type: string + vpcRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - additionalNames + type: object + x-kubernetes-validations: + - message: exactly one UID-pinned vpcRef or resolverContextRef is required + rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() + > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) + && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() + > 0) + - message: a nonempty vpcRef must pin name and UID + rule: '!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) + && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) + || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() + > 0))' + - message: a nonempty resolverContextRef must pin name and UID + rule: '!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() + == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) + || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.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 + resolvedAdditionalNames: + items: + properties: + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + namePrefix: + type: string + registrationClass: + enum: + - InstanceIdentity + - ServiceDiscovery + - ServiceVIP + - ServiceExport + type: string + required: + - dnsZoneRef + - namePrefix + - registrationClass + type: object + type: array + resolvedDNSZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + resolvedResolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + resolvedVPCRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml new file mode 100644 index 00000000..7d2eafee --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -0,0 +1,411 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsrecordcontributions.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSRecordContribution + listKind: DNSRecordContributionList + plural: dnsrecordcontributions + singular: dnsrecordcontribution + 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: + grantRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + recordSets: + items: + properties: + recordType: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + records: + items: + description: RecordEntry represents one owner name and its + values. + properties: + a: + description: Exactly one of the following type-specific + fields should be set matching RecordType. + properties: + content: + format: ipv4 + type: string + required: + - content + type: object + aaaa: + properties: + content: + format: ipv6 + type: string + required: + - content + type: object + alias: + description: |- + ALIASRecordSpec is like CNAME but intended for providers that implement an ALIAS/ANAME-style record. + The content is a hostname (FQDN or relative) with an optional trailing dot. + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + caa: + properties: + flag: + description: 0–255 flag + maximum: 255 + minimum: 0 + type: integer + tag: + description: 'RFC-style tags: keep it simple: [a-z0-9]+' + minLength: 1 + pattern: ^[a-z0-9]+$ + type: string + value: + minLength: 1 + type: string + required: + - flag + - tag + - value + type: object + cname: + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + https: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + mx: + properties: + exchange: + minLength: 1 + type: string + preference: + maximum: 65535 + minimum: 0 + type: integer + required: + - exchange + - preference + type: object + name: + description: Name is the owner name (relative to the zone + or FQDN). + minLength: 1 + pattern: ^(@|[A-Za-z0-9*._-]+)$ + type: string + ns: + properties: + content: + description: |- + Require a hostname (FQDN or relative), allow optional trailing dot, no underscores. + Labels: 1-63 chars, alphanum with interior hyphens, total length <=253. + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\.?$ + type: string + required: + - content + type: object + ptr: + properties: + content: + type: string + required: + - content + type: object + soa: + properties: + expire: + format: int32 + type: integer + mname: + minLength: 1 + type: string + refresh: + format: int32 + type: integer + retry: + format: int32 + type: integer + rname: + minLength: 1 + type: string + serial: + format: int32 + type: integer + ttl: + format: int32 + type: integer + required: + - mname + - rname + type: object + srv: + properties: + port: + maximum: 65535 + minimum: 0 + type: integer + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + minLength: 1 + type: string + weight: + maximum: 65535 + minimum: 0 + type: integer + required: + - port + - priority + - target + - weight + type: object + svcb: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + tlsa: + properties: + certData: + type: string + matchingType: + type: integer + selector: + type: integer + usage: + type: integer + required: + - certData + - matchingType + - selector + - usage + type: object + ttl: + description: TTL optionally overrides TTL for this owner/RRset. + format: int64 + type: integer + txt: + properties: + content: + type: string + required: + - content + type: object + required: + - name + type: object + type: array + required: + - recordType + - records + type: object + type: array + registrationRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - grantRef + - recordSets + - registrationRef + type: object + 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 + eligible: + type: boolean + observedGeneration: + format: int64 + type: integer + publishedRevision: + format: int64 + type: integer + reason: + type: string + sequence: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + 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_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml new file mode 100644 index 00000000..d216c964 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -0,0 +1,176 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsregistrations.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSRegistration + listKind: DNSRegistrationList + plural: dnsregistrations + singular: dnsregistration + 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: + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + name: + type: string + publicationPolicy: + enum: + - EligibleContributions + - Persistent + type: string + recordTypes: + items: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + type: array + reservedDescendants: + items: + type: string + type: array + ttlSeconds: + format: int32 + type: integer + required: + - dnsZoneRef + - name + - publicationPolicy + - recordTypes + - ttlSeconds + type: object + status: + properties: + canonicalFQDN: + type: string + 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 + observedGeneration: + format: int64 + type: integer + publicationRevision: + format: int64 + type: integer + publicationWriterEpoch: + 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_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml new file mode 100644 index 00000000..8436f7dc --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -0,0 +1,194 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsresolveraccessbindings.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverAccessBinding + listKind: DNSResolverAccessBindingList + plural: dnsresolveraccessbindings + singular: dnsresolveraccessbinding + 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: + authorization: + properties: + sequence: + format: int64 + minimum: 1 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + minimum: 1 + type: integer + required: + - sequence + - validUntil + - writerEpoch + type: object + contextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + port: + format: int32 + type: integer + queryIdentity: + properties: + type: + enum: + - DestinationAddress + type: string + value: + type: string + required: + - type + - value + type: object + region: + type: string + transports: + items: + type: string + type: array + required: + - authorization + - contextRef + - port + - queryIdentity + - region + - transports + type: object + status: + properties: + bindingRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + 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 + observedSequence: + 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_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml new file mode 100644 index 00000000..f61da29a --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -0,0 +1,161 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsresolvercontexts.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverContext + listKind: DNSResolverContextList + plural: dnsresolvercontexts + singular: dnsresolvercontext + 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: |- + DNSResolverContext is the DNS-owned serving and namespace boundary created by + a trusted network integration. consumerID is opaque to DNS and immutable. + properties: + consumerID: + type: string + managedNamespace: + properties: + enabled: + type: boolean + type: object + required: + - consumerID + type: object + x-kubernetes-validations: + - message: consumerID is immutable + rule: oldSelf.consumerID == self.consumerID + status: + properties: + accessWriterEpoch: + format: int64 + type: integer + 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 + managedNamespace: + properties: + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + suffix: + type: string + type: object + servingTargets: + items: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml new file mode 100644 index 00000000..eccb6edd --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml @@ -0,0 +1,262 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnszoneassociations.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZoneAssociation + listKind: DNSZoneAssociationList + plural: dnszoneassociations + singular: dnszoneassociation + 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: + dnsZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + managed: + type: boolean + resolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + vpcRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - dnsZoneRef + type: object + x-kubernetes-validations: + - message: exactly one UID-pinned vpcRef or resolverContextRef is required + rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() + > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) + && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() + > 0) + - message: a nonempty vpcRef must pin name and UID + rule: '!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) + && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) + || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() + > 0))' + - message: a nonempty resolverContextRef must pin name and UID + rule: '!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() + == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) + || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() + > 0))' + status: + properties: + bindingRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + 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 + resolvedDNSZoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + resolvedResolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + resolvedVPCRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8fc3e9a2..0cebf89f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -17,3 +17,19 @@ patches: # the following config is for teaching kustomize how to do kustomization for CRDs. #configurations: #- kustomizeconfig.yaml + +- bases/dns.networking.miloapis.com_dnscontributiongrants.yaml + +- bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml + +- bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml + +- bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml + +- bases/dns.networking.miloapis.com_dnsregistrations.yaml + +- bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml + +- bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml + +- bases/dns.networking.miloapis.com_dnszoneassociations.yaml diff --git a/config/internal-dns/examples/context.yaml b/config/internal-dns/examples/context.yaml new file mode 100644 index 00000000..337d38c2 --- /dev/null +++ b/config/internal-dns/examples/context.yaml @@ -0,0 +1,11 @@ +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSResolverContext +metadata: + name: application-network + namespace: project +spec: + # The network integration supplies an opaque, stable consumer identity. + consumerID: network-integration/application-network + # Products discover this namespace instead of asking users to select a zone. + managedNamespace: + enabled: true diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go new file mode 100644 index 00000000..ba6592bb --- /dev/null +++ b/internal/internaldns/admission/handler.go @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Package admission enforces the authenticated writer boundary for internal DNS. +// Compiler validation remains necessary, but cannot infer a Kubernetes caller +// from a stored object's spec. Install this webhook with failurePolicy: Fail. +package admission + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "time" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + "go.miloapis.com/dns-operator/internal/internaldns/model" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + authorizationv1 "k8s.io/api/authorization/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +const Path = "/validate-internal-dns" + +// Handler uses an uncached project client for grant and registration lookups. +// TrustedClusterUID is supplied by the server configuration, never by a record +// publisher. For project-aware deployments, ClientForRequest must resolve the +// authenticated project context and return its immutable source cluster UID. +type Handler struct { + Client client.Client + TrustedClusterUID string + PlatformSubjects []string + IntegrationSubjects []string + MaxContributionLease time.Duration + MaxAccessLease time.Duration + ResolverContextsEnabled bool + Now func() time.Time + ClientForRequest func(context.Context, kubeadmission.Request) (client.Client, string, error) +} + +var _ kubeadmission.Handler = (*Handler)(nil) + +func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadmission.Response { + if req.Resource.Group != dnsv1alpha1.GroupVersion.Group { + return kubeadmission.Denied("unexpected API group") + } + // Public-zone admission remains owned by the existing DNS operator. This + // webhook must not give the internal service exclusive authority over the + // public controller's status writes. + if req.Resource.Resource == "dnszones" { + var zone dnsv1alpha1.DNSZone + if err := decode(req, &zone); err != nil { + return kubeadmission.Denied(err.Error()) + } + if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { + return kubeadmission.Allowed("public DNS admission applies") + } + } + platform := contains(h.PlatformSubjects, req.UserInfo.Username) + integration := contains(h.IntegrationSubjects, req.UserInfo.Username) + if req.Resource.Resource == "dnsresolvercontexts" || req.Resource.Resource == "dnsresolveraccessbindings" { + if req.SubResource == "status" { + if !platform { + return kubeadmission.Denied("resolver context status is DNS platform-owned") + } + return kubeadmission.Allowed("platform writer") + } + if !integration { + return kubeadmission.Denied("resolver context and access specs require a trusted integration writer") + } + cl := h.Client + if h.ClientForRequest != nil { + var err error + cl, _, err = h.ClientForRequest(ctx, req) + if err != nil { + return kubeadmission.Errored(http.StatusServiceUnavailable, err) + } + } + if cl == nil { + return kubeadmission.Errored(http.StatusServiceUnavailable, fmt.Errorf("project API client unavailable")) + } + if req.Operation == admissionv1.Delete { + return kubeadmission.Allowed("integration revocation") + } + var err error + if req.Resource.Resource == "dnsresolvercontexts" { + err = h.resolverContext(req) + } else { + err = h.resolverAccess(ctx, cl, req) + } + if err != nil { + return kubeadmission.Denied(err.Error()) + } + return kubeadmission.Allowed("trusted integration writer") + } + if platformResource(req.Resource.Resource) { + if !platform { + return kubeadmission.Denied("internal serving and publication resources require a platform writer") + } + return kubeadmission.Allowed("platform writer") + } + if req.SubResource == "status" && req.Resource.Resource != "dnsrecordcontributions" && !platform { + return kubeadmission.Denied("DNS publication and writer authority status is platform-owned") + } + if platform { + return kubeadmission.Allowed("platform writer; compiler validates publication inputs") + } + // Deletion revokes intent and cannot introduce serving authority. The API + // server has already authorized delete on this exact project resource. + // Requiring a live grant/registration here would strand retired objects and + // block garbage collection after their parents have been deleted. + if req.Operation == admissionv1.Delete { + return kubeadmission.Allowed("project deletion authorization applies") + } + cl, clusterUID := h.Client, h.TrustedClusterUID + if h.ClientForRequest != nil { + var err error + cl, clusterUID, err = h.ClientForRequest(ctx, req) + if err != nil { + return kubeadmission.Errored(http.StatusServiceUnavailable, err) + } + } + if cl == nil { + return kubeadmission.Errored(http.StatusServiceUnavailable, fmt.Errorf("project API client unavailable")) + } + var err error + switch req.Resource.Resource { + case "dnsregistrations": + err = h.registration(ctx, cl, req) + case "dnsrecordcontributions": + err = h.contribution(ctx, cl, clusterUID, req) + case "dnscontributiongrants": + err = h.grant(ctx, cl, req) + case "dnszoneassociations": + err = h.association(ctx, cl, req) + case "dnsnamingpolicies": + err = h.namingPolicy(ctx, cl, req) + default: + return kubeadmission.Allowed("project API authorization applies") + } + if err != nil { + return kubeadmission.Denied(err.Error()) + } + return kubeadmission.Allowed("authenticated and scoped DNS writer") +} + +func (h *Handler) resolverContext(req kubeadmission.Request) error { + var current, old dnsv1alpha1.DNSResolverContext + if err := decode(req, ¤t); err != nil { + return err + } + if current.Spec.ConsumerID == "" { + return fmt.Errorf("consumerID is required") + } + if len(req.OldObject.Raw) > 0 { + if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { + return err + } + if old.Spec.ConsumerID != current.Spec.ConsumerID { + return fmt.Errorf("consumerID is immutable") + } + if !reflect.DeepEqual(old.Status, current.Status) { + return fmt.Errorf("resolver context status is DNS platform-owned") + } + } + return nil +} + +func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kubeadmission.Request) error { + var current, old dnsv1alpha1.DNSResolverAccessBinding + if err := decode(req, ¤t); err != nil { + return err + } + a := current.Spec.Authorization + if current.Spec.ContextRef.Name == "" || current.Spec.ContextRef.UID == "" { + return fmt.Errorf("access binding must pin context name and UID") + } + var resolverContext dnsv1alpha1.DNSResolverContext + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: current.Spec.ContextRef.Name}, &resolverContext); err != nil { + return fmt.Errorf("resolver context lookup: %w", err) + } + if resolverContext.UID != current.Spec.ContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() { + return fmt.Errorf("resolver context lifetime is not current") + } + if resolverContext.Status.AccessWriterEpoch < 1 || current.Spec.Authorization.WriterEpoch != resolverContext.Status.AccessWriterEpoch { + return fmt.Errorf("access writer epoch is retired or not issued") + } + ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") + if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { + return fmt.Errorf("resolver context is not ready for its current generation") + } + if current.Spec.QueryIdentity.Type != dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress { + return fmt.Errorf("query identity type must be DestinationAddress") + } + if current.Spec.Region == "" || current.Spec.QueryIdentity.Value == "" || current.Spec.Port < 1 || a.WriterEpoch < 1 || a.Sequence < 1 || a.ValidUntil.IsZero() { + return fmt.Errorf("region, destination, port, writer epoch, sequence, and deadline are required") + } + if current.Spec.Port != 53 || !validTransports(current.Spec.Transports) { + return fmt.Errorf("access requires port 53 and exactly UDP and TCP") + } + max := h.MaxAccessLease + if max <= 0 { + max = 5 * time.Minute + } + now := time.Now() + if h.Now != nil { + now = h.Now() + } + if !a.ValidUntil.After(now) { + return fmt.Errorf("access deadline is already expired") + } + if a.ValidUntil.After(now.Add(max)) { + return fmt.Errorf("access deadline exceeds platform lease limit") + } + if len(req.OldObject.Raw) > 0 { + if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { + return err + } + if old.Spec.ContextRef != current.Spec.ContextRef || old.Spec.Region != current.Spec.Region || old.Spec.QueryIdentity != current.Spec.QueryIdentity || old.Spec.Port != current.Spec.Port || !reflect.DeepEqual(old.Spec.Transports, current.Spec.Transports) { + return fmt.Errorf("access identity and transport are immutable; create a new binding") + } + if a.WriterEpoch != old.Spec.Authorization.WriterEpoch { + return fmt.Errorf("writer epoch is immutable for an access binding lifetime") + } + if a.Sequence <= old.Spec.Authorization.Sequence { + return fmt.Errorf("access renewal sequence must increase") + } + if !reflect.DeepEqual(old.Status, current.Status) { + return fmt.Errorf("access binding status is DNS platform-owned") + } + } + return nil +} + +func validTransports(values []string) bool { + if len(values) != 2 { + return false + } + seen := map[string]bool{} + for _, v := range values { + if (v != "UDP" && v != "TCP") || seen[v] { + return false + } + seen[v] = true + } + return seen["UDP"] && seen["TCP"] +} + +func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID string, req kubeadmission.Request) error { + var c, old dnsv1alpha1.DNSRecordContribution + if err := decode(req, &c); err != nil { + return err + } + if c.Spec.GrantRef.UID == "" || c.Spec.RegistrationRef.UID == "" || c.Spec.RegistrationRef.Generation < 1 { + return fmt.Errorf("contribution must pin grant UID and registration UID/generation") + } + var g dnsv1alpha1.DNSContributionGrant + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.GrantRef.Name}, &g); err != nil { + return fmt.Errorf("grant lookup: %w", err) + } + if g.UID != c.Spec.GrantRef.UID || !g.DeletionTimestamp.IsZero() { + return fmt.Errorf("grant lifetime is not current") + } + if clusterUID == "" || g.Spec.Principal.ClusterUID != clusterUID || g.Spec.Principal.Subject != req.UserInfo.Username { + return fmt.Errorf("caller does not match the grant's authenticated producer principal") + } + if g.Spec.RegistrationRef.UID != c.Spec.RegistrationRef.UID || g.Spec.RegistrationRef.Name != c.Spec.RegistrationRef.Name { + return fmt.Errorf("grant belongs to another registration") + } + var r dnsv1alpha1.DNSRegistration + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.RegistrationRef.Name}, &r); err != nil { + return fmt.Errorf("registration lookup: %w", err) + } + if r.UID != c.Spec.RegistrationRef.UID || r.Generation != c.Spec.RegistrationRef.Generation || !r.DeletionTimestamp.IsZero() { + return fmt.Errorf("registration lifetime or policy generation is not current") + } + if g.Status.ActiveWriterEpoch < 1 || g.Status.ObservedRegistrationGeneration != r.Generation || g.Status.ObservedGrantGeneration != g.Generation { + return fmt.Errorf("grant authority is not issued for the current registration generation") + } + var zone dnsv1alpha1.DNSZone + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: r.Spec.DNSZoneRef.Name}, &zone); err != nil { + return fmt.Errorf("private zone lookup: %w", err) + } + if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { + return fmt.Errorf("internal contributions require a private zone") + } + for _, rrset := range c.Spec.RecordSets { + for _, record := range rrset.Records { + if err := model.ValidatePrivateRecordOwner(record.Name, zone.Spec.DomainName, string(rrset.RecordType)); err != nil { + return err + } + } + if !containsType(g.Spec.RecordTypes, rrset.RecordType) || !containsType(r.Spec.RecordTypes, rrset.RecordType) { + return fmt.Errorf("record type %s is outside the writer's scope", rrset.RecordType) + } + } + if len(req.OldObject.Raw) > 0 { + if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { + return err + } + if old.Spec.GrantRef != c.Spec.GrantRef || old.Spec.RegistrationRef != c.Spec.RegistrationRef { + return fmt.Errorf("contribution authority references are immutable; create a new contribution for a new policy lifetime") + } + } + if req.SubResource == "status" { + if !reflect.DeepEqual(old.Status.Conditions, c.Status.Conditions) || old.Status.PublishedRevision != c.Status.PublishedRevision { + return fmt.Errorf("DNS publication status fields are platform-owned") + } + if c.Status.WriterEpoch != g.Status.ActiveWriterEpoch { + return fmt.Errorf("writer epoch is retired or not issued") + } + if c.Status.Sequence <= old.Status.Sequence { + return fmt.Errorf("producer observation sequence must increase") + } + if c.Status.ObservedGeneration != c.Generation { + return fmt.Errorf("producer observation must match the contribution generation") + } + if c.Status.ValidUntil == nil { + return fmt.Errorf("producer observation requires an original freshness deadline") + } + now := time.Now() + if h.Now != nil { + now = h.Now() + } + maxLease := h.MaxContributionLease + if maxLease <= 0 { + maxLease = 90 * time.Second + } + if c.Status.ValidUntil.After(now.Add(maxLease)) { + return fmt.Errorf("freshness deadline exceeds platform lease limit") + } + if c.Status.Eligible && !c.Status.ValidUntil.After(now) { + return fmt.Errorf("eligible observation is already expired") + } + } else if req.Operation != admissionv1.Delete && !reflect.DeepEqual(old.Status, c.Status) && req.Operation != admissionv1.Create { + return fmt.Errorf("producer observations must use the status subresource") + } + return nil +} + +func (h *Handler) grant(ctx context.Context, cl client.Client, req kubeadmission.Request) error { + var g dnsv1alpha1.DNSContributionGrant + if err := decode(req, &g); err != nil { + return err + } + if g.Spec.Principal.Subject == "" || g.Spec.Principal.ClusterUID == "" { + return fmt.Errorf("producer principal is required") + } + if g.Spec.RegistrationRef.UID == "" { + return fmt.Errorf("grant must pin its registration UID") + } + var r dnsv1alpha1.DNSRegistration + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: g.Spec.RegistrationRef.Name}, &r); err != nil { + return err + } + if r.UID != g.Spec.RegistrationRef.UID { + return fmt.Errorf("registration lifetime is not current") + } + return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsregistrations", r.Name, "update") +} + +func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadmission.Request) error { + var a dnsv1alpha1.DNSZoneAssociation + if err := decode(req, &a); err != nil { + return err + } + if a.Spec.Managed { + return fmt.Errorf("managed associations are platform-owned") + } + if err := validateConsumerSelector(a.Spec.VPCRef, a.Spec.ResolverContextRef, h.ResolverContextsEnabled); err != nil { + return err + } + if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", a.Spec.DNSZoneRef.Name, "update"); err != nil { + return err + } + if h.ResolverContextsEnabled { + if a.Spec.ResolverContextRef.UID == "" { + return fmt.Errorf("association must pin resolver context UID") + } + return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsresolvercontexts", a.Spec.ResolverContextRef.Name, "use") + } + return authorize(ctx, cl, req.UserInfo, req.Namespace, "networking.datumapis.com", "networks", a.Spec.VPCRef.Name, "use") +} + +func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubeadmission.Request) error { + var p dnsv1alpha1.DNSNamingPolicy + if err := decode(req, &p); err != nil { + return err + } + if err := validateConsumerSelector(p.Spec.VPCRef, p.Spec.ResolverContextRef, h.ResolverContextsEnabled); err != nil { + return err + } + if p.Spec.DNSZoneRef.Name != "" { + if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", p.Spec.DNSZoneRef.Name, "update"); err != nil { + return err + } + } + for _, rule := range p.Spec.AdditionalNames { + if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", rule.DNSZoneRef.Name, "update"); err != nil { + return err + } + } + if h.ResolverContextsEnabled { + if p.Spec.ResolverContextRef.Name == "" || p.Spec.ResolverContextRef.UID == "" { + return fmt.Errorf("naming policy must pin resolver context name and UID") + } + var resolverContext dnsv1alpha1.DNSResolverContext + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: p.Spec.ResolverContextRef.Name}, &resolverContext); err != nil { + return fmt.Errorf("resolver context lookup: %w", err) + } + ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") + if resolverContext.UID != p.Spec.ResolverContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() || ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { + return fmt.Errorf("resolver context lifetime is not ready") + } + return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsresolvercontexts", resolverContext.Name, "use") + } + return authorize(ctx, cl, req.UserInfo, req.Namespace, "networking.datumapis.com", "networks", p.Spec.VPCRef.Name, "use") +} + +func validateConsumerSelector(vpcRef, contextRef dnsv1alpha1.DNSObjectReference, contextMode bool) error { + hasVPC := vpcRef.Name != "" || vpcRef.UID != "" || vpcRef.Generation != 0 + hasContext := contextRef.Name != "" || contextRef.UID != "" || contextRef.Generation != 0 + if hasVPC == hasContext { + return fmt.Errorf("exactly one of vpcRef or resolverContextRef is required") + } + selected := vpcRef + if hasContext { + selected = contextRef + } + if selected.Name == "" || selected.UID == "" { + return fmt.Errorf("consumer reference must pin name and UID") + } + if contextMode && !hasContext { + return fmt.Errorf("resolver context mode requires resolverContextRef") + } + if !contextMode && !hasVPC { + return fmt.Errorf("legacy networking mode requires vpcRef") + } + return nil +} + +func authorize(ctx context.Context, cl client.Client, user authenticationv1.UserInfo, namespace, group, resource, name, verb string) error { + if name == "" { + return fmt.Errorf("authorization target name is required") + } + extra := map[string]authorizationv1.ExtraValue{} + for k, v := range user.Extra { + extra[k] = authorizationv1.ExtraValue(v) + } + review := &authorizationv1.SubjectAccessReview{ObjectMeta: metav1.ObjectMeta{}, Spec: authorizationv1.SubjectAccessReviewSpec{ + User: user.Username, UID: user.UID, Groups: user.Groups, Extra: extra, + ResourceAttributes: &authorizationv1.ResourceAttributes{Namespace: namespace, Group: group, Resource: resource, Name: name, Verb: verb}, + }} + if err := cl.Create(ctx, review); err != nil { + return fmt.Errorf("authorization review unavailable: %w", err) + } + if !review.Status.Allowed { + return fmt.Errorf("caller is not authorized to %s %s/%s", verb, resource, name) + } + return nil +} + +func decode(req kubeadmission.Request, obj any) error { + data := req.Object.Raw + if req.Operation == admissionv1.Delete { + data = req.OldObject.Raw + } + if len(data) == 0 { + return fmt.Errorf("admission object is missing") + } + return json.Unmarshal(data, obj) +} + +func platformResource(resource string) bool { + switch resource { + case "dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes", "dnspublicationownerships", "dnsmanagednamespaces": + return true + default: + return false + } +} + +func contains(values []string, value string) bool { + for _, v := range values { + if v == value { + return true + } + } + return false +} +func containsType(values []dnsv1alpha1.RRType, value dnsv1alpha1.RRType) bool { + for _, v := range values { + if v == value { + return true + } + } + return false +} + +func (h *Handler) registration(ctx context.Context, cl client.Client, req kubeadmission.Request) error { + var registration dnsv1alpha1.DNSRegistration + if err := decode(req, ®istration); err != nil { + return err + } + var zone dnsv1alpha1.DNSZone + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: registration.Spec.DNSZoneRef.Name}, &zone); err != nil { + return fmt.Errorf("private zone lookup: %w", err) + } + if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { + return fmt.Errorf("internal registrations require a private zone") + } + for _, rt := range registration.Spec.RecordTypes { + if err := model.ValidatePrivateRecordOwner(registration.Spec.Name, zone.Spec.DomainName, string(rt)); err != nil { + return err + } + } + return nil +} diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go new file mode 100644 index 00000000..23fc815f --- /dev/null +++ b/internal/internaldns/admission/handler_test.go @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package admission + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestContributionAuthenticatedWriter(t *testing.T) { + now := time.Date(2026, 10, 6, 18, 0, 0, 0, time.UTC) + producer := "system:serviceaccount:compute-system:dns-publisher" + registration := &dnsv1alpha1.DNSRegistration{ + ObjectMeta: metav1.ObjectMeta{Name: "api", Namespace: "project", UID: "reg-uid", Generation: 3}, + Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: "private"}, RecordTypes: []dnsv1alpha1.RRType{"AAAA"}}, + } + grant := &dnsv1alpha1.DNSContributionGrant{ + ObjectMeta: metav1.ObjectMeta{Name: "compute", Namespace: "project", UID: "grant-uid"}, + Spec: dnsv1alpha1.DNSContributionGrantSpec{ + RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid"}, + Principal: dnsv1alpha1.DNSProducerPrincipal{ClusterUID: "cluster-uid", Subject: producer}, + RecordTypes: []dnsv1alpha1.RRType{"AAAA"}, + }, + Status: dnsv1alpha1.DNSContributionGrantStatus{ActiveWriterEpoch: 8, ObservedRegistrationGeneration: 3}, + } + contribution := dnsv1alpha1.DNSRecordContribution{ + ObjectMeta: metav1.ObjectMeta{Name: "api-east", Namespace: "project", UID: "contribution-uid", Generation: 2}, + Spec: dnsv1alpha1.DNSRecordContributionSpec{ + RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid", Generation: 3}, + GrantRef: dnsv1alpha1.DNSObjectReference{Name: "compute", UID: "grant-uid"}, + }, + Status: dnsv1alpha1.DNSRecordContributionStatus{ObservedGeneration: 2, WriterEpoch: 8, Sequence: 18, Eligible: true, ValidUntil: ptrTime(now.Add(30 * time.Second))}, + } + scheme := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(registration, grant, &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}}).Build(), TrustedClusterUID: "cluster-uid", Now: func() time.Time { return now }} + cases := []struct { + name string + user string + mutate func(*dnsv1alpha1.DNSRecordContribution) + allowed bool + }{ + {"scoped fresh producer", producer, nil, true}, + {"another producer", "system:serviceaccount:connector-system:dns-publisher", nil, false}, + {"retired epoch", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 7 }, false}, + {"replayed sequence", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 17 }, false}, + {"stale generation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ObservedGeneration = 1 }, false}, + {"forged grant UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.GrantRef.UID = "other-grant" }, false}, + {"retired registration UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.RegistrationRef.UID = "old-reg" }, false}, + {"publication fields", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.PublishedRevision = 1000 }, false}, + {"overlong lease", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ValidUntil = ptrTime(now.Add(time.Hour)) }, false}, + {"expired eligible lease", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ValidUntil = ptrTime(now.Add(-time.Second)) }, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := contribution.DeepCopy() + old := contribution.DeepCopy() + old.Status.Sequence = 17 + if tc.mutate != nil { + tc.mutate(c) + } + req := request("dnsrecordcontributions", tc.user, c, old) + req.SubResource = "status" + res := h.Handle(context.Background(), req) + if res.Allowed != tc.allowed { + t.Fatalf("allowed=%v want=%v: %v", res.Allowed, tc.allowed, res.Result) + } + }) + } +} + +func TestPlatformAuthorityCannotBeWrittenByProducer(t *testing.T) { + h := Handler{PlatformSubjects: []string{"system:serviceaccount:dns-system:control-plane"}} + for _, resource := range []string{"dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes"} { + req := request(resource, "system:serviceaccount:compute-system:dns-publisher", map[string]any{}, nil) + if h.Handle(context.Background(), req).Allowed { + t.Fatalf("producer allowed to write %s", resource) + } + req.UserInfo.Username = h.PlatformSubjects[0] + if !h.Handle(context.Background(), req).Allowed { + t.Fatalf("platform writer denied %s", resource) + } + } + req := request("dnscontributiongrants", "system:serviceaccount:compute-system:dns-publisher", map[string]any{}, nil) + req.SubResource = "status" + if h.Handle(context.Background(), req).Allowed { + t.Fatal("producer allowed to issue own epoch") + } +} + +func TestUnavailableProjectContextFailsClosed(t *testing.T) { + h := Handler{} + res := h.Handle(context.Background(), request("dnsrecordcontributions", "publisher", map[string]any{}, nil)) + if res.Allowed || res.Result == nil || res.Result.Code != 503 { + t.Fatalf("must fail closed: %#v", res) + } +} + +func ptrTime(t time.Time) *metav1.Time { v := metav1.NewTime(t); return &v } + +func request(resource, username string, obj, old any) kubeadmission.Request { + raw, _ := json.Marshal(obj) + var oldRaw []byte + if old != nil { + oldRaw, _ = json.Marshal(old) + } + return kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + UID: types.UID("request-uid"), Namespace: "project", Operation: admissionv1.Update, + Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: resource}, + Kind: metav1.GroupVersionKind{Group: schema.GroupVersion(dnsv1alpha1.GroupVersion).Group, Version: "v1alpha1"}, + UserInfo: authenticationv1.UserInfo{Username: username}, Object: runtime.RawExtension{Raw: raw}, OldObject: runtime.RawExtension{Raw: oldRaw}, + }} +} + +func TestInternalAdmissionPreservesPublicZoneControllerStatus(t *testing.T) { + h := Handler{PlatformSubjects: []string{"system:serviceaccount:internal-dns-system:controller"}} + req := kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Update, Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: "dnszones"}, SubResource: "status", UserInfo: authenticationv1.UserInfo{Username: "system:serviceaccount:dns-system:public-controller"}}} + zone := dnsv1alpha1.DNSZone{Spec: dnsv1alpha1.DNSZoneSpec{Visibility: dnsv1alpha1.DNSZoneVisibilityPublic}} + req.Object.Raw, _ = json.Marshal(zone) + if !h.Handle(context.Background(), req).Allowed { + t.Fatal("internal admission blocked legacy public-zone status") + } + zone.Spec.Visibility = dnsv1alpha1.DNSZoneVisibilityPrivate + req.Object.Raw, _ = json.Marshal(zone) + if h.Handle(context.Background(), req).Allowed { + t.Fatal("legacy public writer received private publication authority") + } +} + +func TestRetiredProductIntentCanBeDeletedWithoutLiveGrant(t *testing.T) { + h := Handler{} + req := kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete, Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: "dnsrecordcontributions"}, UserInfo: authenticationv1.UserInfo{Username: "system:serviceaccount:compute:publisher"}}} + if !h.Handle(context.Background(), req).Allowed { + t.Fatal("retired contribution cleanup required a live deleted grant") + } + req.Resource.Resource = "dnspublicationownerships" + if h.Handle(context.Background(), req).Allowed { + t.Fatal("product cleanup acquired platform ownership deletion authority") + } +} + +func TestResolverAccessRequiresIntegrationAndMonotonicBoundedLease(t *testing.T) { + now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) + scheme := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessWriterEpoch: 3}} + setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build(), IntegrationSubjects: []string{"system:serviceaccount:network-system:integration"}, MaxAccessLease: 5 * time.Minute, Now: func() time.Time { return now }} + old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: dnsv1alpha1.DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 9, ValidUntil: metav1.NewTime(now.Add(2 * time.Minute))}}} + current := old.DeepCopy() + current.Spec.Authorization.Sequence = 10 + current.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(3 * time.Minute)) + req := request("dnsresolveraccessbindings", h.IntegrationSubjects[0], current, old) + if res := h.Handle(context.Background(), req); !res.Allowed { + t.Fatalf("fresh integration renewal denied: %v", res.Result) + } + req.UserInfo.Username = "system:serviceaccount:compute:publisher" + if h.Handle(context.Background(), req).Allowed { + t.Fatal("publisher granted network access") + } + req.UserInfo.Username = h.IntegrationSubjects[0] + replay := old.DeepCopy() + req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], replay, old) + if h.Handle(context.Background(), req).Allowed { + t.Fatal("replayed sequence accepted") + } + overlong := current.DeepCopy() + overlong.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(6 * time.Minute)) + req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], overlong, old) + if h.Handle(context.Background(), req).Allowed { + t.Fatal("overlong access lease accepted") + } + forged := current.DeepCopy() + forged.Spec.ContextRef.UID = "replacement" + req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], forged, old) + if h.Handle(context.Background(), req).Allowed { + t.Fatal("context lifetime changed in place") + } + staleContext := resolverContext.DeepCopy() + staleContext.Generation = 2 + h.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(staleContext).Build() + req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], current, old) + if h.Handle(context.Background(), req).Allowed { + t.Fatal("stale context Ready condition authorized access") + } +} + +func TestConsumerSelectorRejectsBothOrEmptyInEveryMode(t *testing.T) { + vpc := dnsv1alpha1.DNSObjectReference{Name: "vpc", UID: "vpc-uid"} + resolverContext := dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"} + for _, contextMode := range []bool{false, true} { + for _, tc := range []struct { + name string + vpc, context dnsv1alpha1.DNSObjectReference + }{{"empty", dnsv1alpha1.DNSObjectReference{}, dnsv1alpha1.DNSObjectReference{}}, {"both", vpc, resolverContext}, {"unpinned vpc", dnsv1alpha1.DNSObjectReference{Name: "vpc"}, dnsv1alpha1.DNSObjectReference{}}, {"unpinned context", dnsv1alpha1.DNSObjectReference{}, dnsv1alpha1.DNSObjectReference{Name: "ctx"}}} { + t.Run(fmt.Sprintf("context=%v/%s", contextMode, tc.name), func(t *testing.T) { + if err := validateConsumerSelector(tc.vpc, tc.context, contextMode); err == nil { + t.Fatal("ambiguous or unpinned selector accepted") + } + }) + } + } + for _, tc := range []struct { + name string + vpc, context dnsv1alpha1.DNSObjectReference + }{{"complete vpc partial context", vpc, dnsv1alpha1.DNSObjectReference{Name: "ctx"}}, {"partial vpc complete context", dnsv1alpha1.DNSObjectReference{UID: "vpc-uid"}, resolverContext}} { + if err := validateConsumerSelector(tc.vpc, tc.context, true); err == nil { + t.Fatalf("%s accepted in context mode", tc.name) + } + if err := validateConsumerSelector(tc.vpc, tc.context, false); err == nil { + t.Fatalf("%s accepted in legacy mode", tc.name) + } + } + if err := validateConsumerSelector(vpc, dnsv1alpha1.DNSObjectReference{}, false); err != nil { + t.Fatalf("legacy selector rejected: %v", err) + } + if err := validateConsumerSelector(dnsv1alpha1.DNSObjectReference{}, resolverContext, true); err != nil { + t.Fatalf("context selector rejected: %v", err) + } +} + +func setAdmissionCondition(conditions *[]metav1.Condition, conditionType string, status metav1.ConditionStatus, generation int64, now time.Time) { + apimeta.SetStatusCondition(conditions, metav1.Condition{Type: conditionType, Status: status, Reason: "Test", ObservedGeneration: generation, LastTransitionTime: metav1.NewTime(now)}) +} + +func TestPrivateRegistrationRejectsAuthorityOverrides(t *testing.T) { + scheme := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone).Build()} + for _, tt := range []struct { + owner string + rt dnsv1alpha1.RRType + allowed bool + }{{"api", dnsv1alpha1.RRTypeA, true}, {"api", dnsv1alpha1.RRTypeCNAME, true}, {"@", dnsv1alpha1.RRTypeCNAME, false}, {"CORP.INTERNAL.", dnsv1alpha1.RRTypeCNAME, false}, {"@", dnsv1alpha1.RRTypeSOA, false}} { + registration := dnsv1alpha1.DNSRegistration{Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name}, Name: tt.owner, RecordTypes: []dnsv1alpha1.RRType{tt.rt}}} + response := h.Handle(context.Background(), request("dnsregistrations", "tenant", registration, nil)) + if response.Allowed != tt.allowed { + t.Fatalf("owner %s type %s allowed=%v expected=%v", tt.owner, tt.rt, response.Allowed, tt.allowed) + } + } +} diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go new file mode 100644 index 00000000..2f65c3c9 --- /dev/null +++ b/internal/internaldns/model/model.go @@ -0,0 +1,547 @@ +// Package model defines the durable wire contract shared by the internal DNS +// control plane and every regional serving replica. +package model + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/netip" + "regexp" + "sort" + "strings" + "time" + + "github.com/miekg/dns" +) + +const APIVersion = "internal.dns.networking.miloapis.com/v1alpha2" + +type Kind string + +const ( + KindServingSnapshot Kind = "ServingSnapshot" + KindPublicationChunk Kind = "PublicationChunk" + KindPublicationManifest Kind = "PublicationManifest" + KindMemberAck Kind = "MemberAck" +) + +// Envelope is the only value written to JetStream. Epoch and Revision are +// repeated outside Payload so consumers can reject stale events before decode. +type Envelope struct { + APIVersion string `json:"apiVersion"` + Kind Kind `json:"kind"` + EventID string `json:"eventID"` + Region string `json:"region"` + Shard string `json:"shard"` + ResourceUID string `json:"resourceUID"` + Epoch uint64 `json:"epoch"` + Revision uint64 `json:"revision"` + PublishedAt time.Time `json:"publishedAt"` + Payload json.RawMessage `json:"payload"` +} + +func NewEnvelope(kind Kind, eventID, region, shard, uid string, epoch, revision uint64, at time.Time, payload any) (Envelope, error) { + b, err := json.Marshal(payload) + if err != nil { + return Envelope{}, fmt.Errorf("marshal %s payload: %w", kind, err) + } + e := Envelope{APIVersion: APIVersion, Kind: kind, EventID: eventID, Region: region, Shard: shard, ResourceUID: uid, Epoch: epoch, Revision: revision, PublishedAt: at.UTC(), Payload: b} + return e, e.Validate() +} + +func (e Envelope) Validate() error { + if e.APIVersion != APIVersion { + return fmt.Errorf("unsupported apiVersion %q", 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 nil +} + +type Transport string + +const ( + TransportUDP Transport = "UDP" + TransportTCP Transport = "TCP" +) + +type Authorization struct { + IssuerEpoch uint64 `json:"issuerEpoch"` + Revision uint64 `json:"revision"` + ValidUntil time.Time `json:"validUntil"` +} + +type Backend struct { + MemberID string `json:"memberID"` + Address string `json:"address"` + Port uint16 `json:"port"` +} + +type ZoneAttachment struct { + ZoneUID string `json:"zoneUID"` + Apex string `json:"apex"` + RequiredPublicationEpoch uint64 `json:"requiredPublicationEpoch"` + RequiredPublicationRevision uint64 `json:"requiredPublicationRevision"` +} + +// Binding is a logical cache and policy context in shared processes. +type Binding struct { + BindingUID string `json:"bindingUID"` + ProjectUID string `json:"projectUID"` + ContextUID string `json:"contextUID"` + BindingGeneration uint64 `json:"bindingGeneration"` + ConfigurationRevision uint64 `json:"configurationRevision"` + ConsumerAddress string `json:"consumerAddress"` + ClusterAddress string `json:"clusterAddress"` + Port uint16 `json:"port"` + Transports []Transport `json:"transports"` + Zones []ZoneAttachment `json:"zones"` + NodeBackends []Backend `json:"nodeBackends"` + ClusterBackends []Backend `json:"clusterBackends"` + Authorization Authorization `json:"authorization"` + Tombstone bool `json:"tombstone,omitempty"` +} + +func (b Binding) ViewName() string { + return "context_" + OpaqueToken(b.ProjectUID+"/"+b.ContextUID) + "_g" + fmt.Sprint(b.BindingGeneration) +} + +type Member struct { + MemberID string `json:"memberID"` + ReplicaID string `json:"replicaID,omitempty"` + Role string `json:"role"` +} + +type PublicationRequirement struct { + ZoneUID string `json:"zoneUID"` + Epoch uint64 `json:"epoch"` + Revision uint64 `json:"revision"` +} + +// ServingSnapshot is complete for a shard. Omission therefore means removal, +// but only after the entire snapshot has passed validation and been staged. +type ServingSnapshot struct { + Region string `json:"region"` + Shard string `json:"shard"` + ConfigurationEpoch uint64 `json:"configurationEpoch"` + ConfigurationRevision uint64 `json:"configurationRevision"` + Bindings []Binding `json:"bindings"` + Members []Member `json:"members"` + RequiredPublications []PublicationRequirement `json:"requiredPublications,omitempty"` + GeneratedAt time.Time `json:"generatedAt"` +} + +func (s ServingSnapshot) Validate() error { + if s.Region == "" || s.Shard == "" || s.ConfigurationEpoch == 0 || s.ConfigurationRevision == 0 || s.GeneratedAt.IsZero() { + return errors.New("serving snapshot identity, epoch, revision, and timestamp are required") + } + + seenVIP := map[string]string{} + seenUID := map[string]struct{}{} + seenMembers := map[string]struct{}{} + for _, member := range s.Members { + if member.MemberID == "" || (member.Role != "resolver" && member.Role != "node" && member.Role != "cluster" && member.Role != "regional") { + return fmt.Errorf("invalid serving member %q with role %q", member.MemberID, member.Role) + } + key := member.MemberID + "\x00" + member.ReplicaID + if _, ok := seenMembers[key]; ok { + return fmt.Errorf("duplicate serving member %q replica %q", member.MemberID, member.ReplicaID) + } + seenMembers[key] = struct{}{} + } + seenRequirements := map[string]struct{}{} + for _, requirement := range s.RequiredPublications { + if requirement.ZoneUID == "" || requirement.Epoch == 0 || requirement.Revision == 0 { + return errors.New("required publication has incomplete identity") + } + if _, ok := seenRequirements[requirement.ZoneUID]; ok { + return fmt.Errorf("duplicate required publication %q", requirement.ZoneUID) + } + seenRequirements[requirement.ZoneUID] = struct{}{} + } + for i, b := range s.Bindings { + if b.BindingUID == "" || b.ProjectUID == "" || b.ContextUID == "" || b.BindingGeneration == 0 || b.ConfigurationRevision == 0 { + return fmt.Errorf("binding %d has incomplete identity", i) + } + if _, ok := seenUID[b.BindingUID]; ok { + return fmt.Errorf("duplicate binding UID %q", b.BindingUID) + } + seenUID[b.BindingUID] = struct{}{} + for name, raw := range map[string]string{"consumerAddress": b.ConsumerAddress, "clusterAddress": b.ClusterAddress} { + addr, err := netip.ParseAddr(raw) + if err != nil || addr.IsUnspecified() { + return fmt.Errorf("binding %q invalid %s %q", b.BindingUID, name, raw) + } + if old, ok := seenVIP[raw]; ok && old != b.BindingUID { + return fmt.Errorf("address %s assigned to bindings %q and %q", raw, old, b.BindingUID) + } + seenVIP[raw] = b.BindingUID + } + if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 || b.Authorization.ValidUntil.IsZero() { + return fmt.Errorf("binding %q has incomplete transport or authorization", b.BindingUID) + } + if err := validateTransports(b.Transports); err != nil { + return fmt.Errorf("binding %q: %w", b.BindingUID, err) + } + for _, group := range [][]Backend{b.NodeBackends, b.ClusterBackends} { + backendMembers := map[string]string{} + backendAddresses := map[string]string{} + for _, backend := range group { + if backend.MemberID == "" || backend.Port == 0 { + return fmt.Errorf("binding %q has incomplete backend", b.BindingUID) + } + if addr, err := netip.ParseAddr(backend.Address); err != nil || addr.IsUnspecified() { + return fmt.Errorf("binding %q has invalid backend address %q", b.BindingUID, backend.Address) + } + endpoint := netip.MustParseAddr(backend.Address).String() + ":" + fmt.Sprint(backend.Port) + if old, ok := backendMembers[backend.MemberID]; ok && old != endpoint { + return fmt.Errorf("binding %q backend member %q has conflicting endpoints", b.BindingUID, backend.MemberID) + } + if old, ok := backendAddresses[endpoint]; ok && old != backend.MemberID { + return fmt.Errorf("binding %q backend endpoint %q has conflicting member IDs", b.BindingUID, endpoint) + } + backendMembers[backend.MemberID] = endpoint + backendAddresses[endpoint] = backend.MemberID + } + } + zoneApex := map[string]string{} + for _, z := range b.Zones { + if z.ZoneUID == "" || z.RequiredPublicationEpoch == 0 || z.RequiredPublicationRevision == 0 { + return fmt.Errorf("binding %q has incomplete zone attachment", b.BindingUID) + } + apex := AbsoluteName(z.Apex) + if _, ok := dns.IsDomainName(apex); apex == "." || !ok { + return fmt.Errorf("binding %q has invalid zone apex", b.BindingUID) + } + if old, ok := zoneApex[apex]; ok && old != z.ZoneUID { + return fmt.Errorf("binding %q attaches zones %q and %q at %s", b.BindingUID, old, z.ZoneUID, apex) + } + zoneApex[apex] = z.ZoneUID + } + } + return nil +} + +func validateTransports(ts []Transport) error { + seen := map[Transport]bool{} + for _, t := range ts { + if t != TransportUDP && t != TransportTCP { + return fmt.Errorf("unsupported transport %q", t) + } + if seen[t] { + return fmt.Errorf("duplicate transport %q", t) + } + seen[t] = true + } + return nil +} + +type ChunkRef struct { + Index int `json:"index"` + SHA256 string `json:"sha256"` + Size int `json:"size"` +} + +type PublicationManifest struct { + ManifestUID string `json:"manifestUID"` + ZoneUID string `json:"zoneUID"` + Apex string `json:"apex"` + WriterEpoch uint64 `json:"writerEpoch"` + Revision uint64 `json:"revision"` + PreviousRevision uint64 `json:"previousRevision,omitempty"` + Tombstone bool `json:"tombstone,omitempty"` + Chunks []ChunkRef `json:"chunks"` + ContentHash string `json:"contentHash"` + GeneratedAt time.Time `json:"generatedAt"` +} + +func (m PublicationManifest) Validate() error { + if m.ManifestUID == "" || m.ZoneUID == "" || AbsoluteName(m.Apex) == "." || m.WriterEpoch == 0 || m.Revision == 0 || m.GeneratedAt.IsZero() { + return errors.New("publication manifest identity, zone, epoch, revision, and timestamp are required") + } + if m.PreviousRevision >= m.Revision && m.PreviousRevision != 0 { + return errors.New("publication manifest previous revision must precede revision") + } + if _, ok := dns.IsDomainName(AbsoluteName(m.Apex)); !ok { + return errors.New("publication manifest apex is invalid") + } + if m.Tombstone { + if len(m.Chunks) != 0 || m.ContentHash != EmptyContentHash() { + return errors.New("tombstone must have no chunks and the empty content hash") + } + return nil + } + if len(m.Chunks) == 0 || !validHash(m.ContentHash) { + return errors.New("non-tombstone manifest requires chunks and a SHA-256 content hash") + } + refs := append([]ChunkRef(nil), m.Chunks...) + sort.Slice(refs, func(i, j int) bool { return refs[i].Index < refs[j].Index }) + for i, ref := range refs { + if ref.Index != i || ref.Size <= 0 || !validHash(ref.SHA256) { + return fmt.Errorf("invalid chunk reference at ordered index %d", i) + } + } + return nil +} + +type PublicationChunk struct { + ManifestUID string `json:"manifestUID"` + ZoneUID string `json:"zoneUID"` + WriterEpoch uint64 `json:"writerEpoch"` + Revision uint64 `json:"revision"` + Index int `json:"index"` + SHA256 string `json:"sha256"` + Payload []byte `json:"payload"` +} + +func (c PublicationChunk) Validate() error { + if c.ManifestUID == "" || c.ZoneUID == "" || c.WriterEpoch == 0 || c.Revision == 0 || c.Index < 0 || len(c.Payload) == 0 || !validHash(c.SHA256) { + return errors.New("publication chunk identity, revision, hash, and payload are required") + } + if Hash(c.Payload) != c.SHA256 { + return errors.New("publication chunk SHA-256 mismatch") + } + return nil +} + +type RRRecord struct { + Content string `json:"content"` + ContributionUID string `json:"contributionUID,omitempty"` + WriterEpoch uint64 `json:"writerEpoch,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + Eligible bool `json:"eligible"` + ValidUntil time.Time `json:"validUntil,omitempty"` +} + +type RRSet struct { + Name string `json:"name"` + Type string `json:"type"` + TTL uint32 `json:"ttl"` + Records []RRRecord `json:"records"` +} + +type PublicationPlan struct { + ZoneUID string `json:"zoneUID"` + Apex string `json:"apex"` + Registrations []RegistrationFence `json:"registrations,omitempty"` + ObservationFences []ObservationFence `json:"observationFences,omitempty"` + Owners []string `json:"owners,omitempty"` + RRSets []RRSet `json:"rrsets"` +} + +type RegistrationFence struct { + UID string `json:"uid"` + Generation uint64 `json:"generation"` +} + +// ObservationFence carries the accepted high-water mark even when an +// unhealthy or withdrawn observation produces no records. ValidUntil is the +// original contributor deadline and is part of the immutable fence identity. +type ObservationFence struct { + ContributionUID string `json:"contributionUID"` + GrantUID string `json:"grantUID"` + WriterEpoch uint64 `json:"writerEpoch"` + Sequence uint64 `json:"sequence"` + ValidUntil time.Time `json:"validUntil"` +} + +func (p PublicationPlan) Validate() error { + if p.ZoneUID == "" || AbsoluteName(p.Apex) == "." { + return errors.New("publication plan zone identity is required") + } + apex := AbsoluteName(p.Apex) + if _, ok := dns.IsDomainName(apex); !ok { + return errors.New("publication plan apex is invalid") + } + previousRegistration := "" + for _, registration := range p.Registrations { + if registration.UID == "" || registration.Generation == 0 { + return errors.New("publication registration fence is incomplete") + } + if previousRegistration != "" && registration.UID <= previousRegistration { + return errors.New("publication registration fences must be unique and sorted by UID") + } + previousRegistration = registration.UID + } + observationByUID := make(map[string]ObservationFence, len(p.ObservationFences)) + previousContribution := "" + for _, observation := range p.ObservationFences { + if observation.ContributionUID == "" || observation.GrantUID == "" || observation.WriterEpoch == 0 || observation.Sequence == 0 || observation.ValidUntil.IsZero() { + return errors.New("publication observation fence is incomplete") + } + if previousContribution != "" && observation.ContributionUID <= previousContribution { + return errors.New("publication observation fences must be unique and sorted by contribution UID") + } + previousContribution = observation.ContributionUID + observationByUID[observation.ContributionUID] = observation + } + for _, owner := range p.Owners { + owner = AbsoluteName(owner) + if _, ok := dns.IsDomainName(owner); !ok || !dns.IsSubDomain(apex, owner) { + return fmt.Errorf("owned name %q is outside zone %q", owner, apex) + } + } + seen := map[string]struct{}{} + for _, rr := range p.RRSets { + owner := strings.ToLower(AbsoluteName(rr.Name)) + rrType := strings.ToUpper(rr.Type) + key := owner + "\x00" + rrType + if rr.TTL == 0 || len(rr.Records) == 0 || rr.Type == "" { + return fmt.Errorf("RRset %q is incomplete", key) + } + if _, ok := dns.IsDomainName(owner); !ok || !dns.IsSubDomain(apex, owner) { + return fmt.Errorf("RRset owner %q is outside zone %q", owner, apex) + } + if _, ok := dns.StringToType[rrType]; !ok { + return fmt.Errorf("RRset %q has unsupported type %q", owner, rr.Type) + } + if _, ok := seen[key]; ok { + return fmt.Errorf("duplicate RRset %q", key) + } + seen[key] = struct{}{} + for _, rec := range rr.Records { + if rec.Content == "" { + return fmt.Errorf("RRset %q contains an invalid record", key) + } + if rec.ContributionUID == "" { + if rec.WriterEpoch != 0 || rec.Sequence != 0 || !rec.ValidUntil.IsZero() { + return fmt.Errorf("RRset %q static record contains contribution fence data", key) + } + } else { + observation, ok := observationByUID[rec.ContributionUID] + if !ok || rec.WriterEpoch != observation.WriterEpoch || rec.Sequence != observation.Sequence || !rec.ValidUntil.Equal(observation.ValidUntil) { + return fmt.Errorf("RRset %q record fence does not match contribution observation %q", key, rec.ContributionUID) + } + } + if _, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", owner, rr.TTL, rrType, rec.Content)); err != nil { + return fmt.Errorf("RRset %q contains invalid RDATA: %w", key, err) + } + } + } + return nil +} + +type AckPhase string + +const ( + AckAccepted AckPhase = "Accepted" + AckApplied AckPhase = "Applied" + AckVerified AckPhase = "Verified" + AckExpired AckPhase = "Expired" + AckRejected AckPhase = "Rejected" +) + +type MemberAck struct { + MemberID string `json:"memberID"` + ReplicaID string `json:"replicaID,omitempty"` + ResourceUID string `json:"resourceUID"` + Kind Kind `json:"kind"` + Epoch uint64 `json:"epoch"` + Revision uint64 `json:"revision"` + Phase AckPhase `json:"phase"` + ObservedAt time.Time `json:"observedAt"` + ValidUntil time.Time `json:"validUntil,omitempty"` + Error string `json:"error,omitempty"` +} + +func Hash(b []byte) string { + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} + +func EmptyContentHash() string { return Hash(nil) } + +func VerifyManifest(m PublicationManifest, chunks []PublicationChunk) ([]byte, error) { + if err := m.Validate(); err != nil { + return nil, err + } + if m.Tombstone { + return nil, nil + } + if len(chunks) != len(m.Chunks) { + return nil, fmt.Errorf("manifest references %d chunks but %d were supplied", len(m.Chunks), len(chunks)) + } + byIndex := make(map[int]PublicationChunk, len(chunks)) + for _, c := range chunks { + if err := c.Validate(); err != nil { + return nil, err + } + if c.ManifestUID != m.ManifestUID || c.ZoneUID != m.ZoneUID || c.WriterEpoch != m.WriterEpoch || c.Revision != m.Revision { + return nil, fmt.Errorf("chunk %d identity does not match manifest", c.Index) + } + if _, exists := byIndex[c.Index]; exists { + return nil, fmt.Errorf("duplicate chunk index %d", c.Index) + } + byIndex[c.Index] = c + } + var payload []byte + for _, ref := range m.Chunks { + c, ok := byIndex[ref.Index] + if !ok || len(c.Payload) != ref.Size || c.SHA256 != ref.SHA256 { + return nil, fmt.Errorf("chunk %d is missing or differs from manifest", ref.Index) + } + payload = append(payload, c.Payload...) + } + if Hash(payload) != m.ContentHash { + return nil, errors.New("publication content SHA-256 mismatch") + } + return payload, nil +} + +func Compare(epoch, revision, currentEpoch, currentRevision uint64) int { + if epoch < currentEpoch || (epoch == currentEpoch && revision < currentRevision) { + return -1 + } + if epoch == currentEpoch && revision == currentRevision { + return 0 + } + return 1 +} + +func AbsoluteName(s string) string { + s = strings.ToLower(strings.TrimSpace(s)) + if s == "" { + return "." + } + if !strings.HasSuffix(s, ".") { + s += "." + } + return s +} + +var unsafeToken = regexp.MustCompile(`[^a-z0-9_-]+`) + +func SafeToken(s string) string { + s = unsafeToken.ReplaceAllString(strings.ToLower(s), "_") + s = strings.Trim(s, "_") + if s == "" { + return "unknown" + } + return s +} + +// OpaqueToken avoids placing customer-controlled identifiers in NATS subjects. +func OpaqueToken(uid string) string { + sum := sha256.Sum256([]byte(uid)) + return hex.EncodeToString(sum[:12]) +} + +func validHash(s string) bool { + if len(s) != sha256.Size*2 { + return false + } + _, err := hex.DecodeString(s) + return err == nil +} diff --git a/internal/internaldns/model/model_test.go b/internal/internaldns/model/model_test.go new file mode 100644 index 00000000..ef88d0fb --- /dev/null +++ b/internal/internaldns/model/model_test.go @@ -0,0 +1,93 @@ +package model + +import ( + "encoding/json" + "testing" + "time" +) + +func TestVerifyManifestRequiresEveryExactChunk(t *testing.T) { + t.Parallel() + plan := PublicationPlan{ZoneUID: "zone-a", Apex: "internal.example", RRSets: []RRSet{{Name: "api.internal.example", Type: "A", TTL: 30, Records: []RRRecord{{Content: "192.0.2.1", Eligible: true}}}}} + payload, err := json.Marshal(plan) + if err != nil { + t.Fatal(err) + } + cut := len(payload) / 2 + parts := [][]byte{payload[:cut], payload[cut:]} + chunks := make([]PublicationChunk, 0, 2) + refs := make([]ChunkRef, 0, 2) + for i, p := range parts { + chunks = append(chunks, PublicationChunk{ManifestUID: "manifest-a", ZoneUID: "zone-a", WriterEpoch: 3, Revision: 9, Index: i, SHA256: Hash(p), Payload: p}) + refs = append(refs, ChunkRef{Index: i, SHA256: Hash(p), Size: len(p)}) + } + m := PublicationManifest{ManifestUID: "manifest-a", ZoneUID: "zone-a", Apex: "internal.example", WriterEpoch: 3, Revision: 9, Chunks: refs, ContentHash: Hash(payload), GeneratedAt: time.Now()} + got, err := VerifyManifest(m, chunks) + if err != nil { + t.Fatal(err) + } + if string(got) != string(payload) { + t.Fatalf("payload differs") + } + if _, err := VerifyManifest(m, chunks[:1]); err == nil { + t.Fatal("missing chunk accepted") + } + chunks[1].Payload[0] ^= 1 + if _, err := VerifyManifest(m, chunks); err == nil { + t.Fatal("corrupt chunk accepted") + } +} + +func TestServingSnapshotRejectsSharedDestination(t *testing.T) { + t.Parallel() + now := time.Now() + b := func(uid, vpc string) Binding { + return Binding{BindingUID: uid, ProjectUID: "project", ContextUID: vpc, BindingGeneration: 1, ConfigurationRevision: 1, ConsumerAddress: "2001:db8::1", ClusterAddress: "2001:db8::2", Port: 53, Transports: []Transport{TransportUDP, TransportTCP}, Authorization: Authorization{IssuerEpoch: 1, Revision: 1, ValidUntil: now.Add(time.Minute)}} + } + s := ServingSnapshot{Region: "east", Shard: "s1", ConfigurationEpoch: 1, ConfigurationRevision: 1, GeneratedAt: now, Bindings: []Binding{b("one", "vpc-a"), b("two", "vpc-b")}} + if err := s.Validate(); err == nil { + t.Fatal("duplicate trusted destination accepted") + } +} + +func TestServingSnapshotRejectsUnknownMemberRole(t *testing.T) { + s := ServingSnapshot{Region: "east", Shard: "s1", ConfigurationEpoch: 1, ConfigurationRevision: 1, GeneratedAt: time.Now(), Members: []Member{{MemberID: "old", Role: "unassigned"}}} + if s.Validate() == nil { + t.Fatal("unknown serving role accepted") + } +} + +func TestCompareFencesOldEpoch(t *testing.T) { + t.Parallel() + if Compare(2, 1000, 3, 1) >= 0 { + t.Fatal("old epoch won by revision") + } + if Compare(4, 1, 3, 1000) <= 0 { + t.Fatal("new epoch lost") + } +} + +func TestViewNameScopesContextByProject(t *testing.T) { + a := Binding{ProjectUID: "project-a", ContextUID: "remote-context", BindingGeneration: 1} + b := a + b.ProjectUID = "project-b" + if a.ViewName() == b.ViewName() { + t.Fatal("identical remote context UID shared a cache namespace across projects") + } + b = a + b.BindingGeneration = 2 + if a.ViewName() == b.ViewName() { + t.Fatal("new binding generation reused an old cache namespace") + } +} + +func TestEnvelopeRejectsObsoleteWireVersion(t *testing.T) { + current, err := NewEnvelope(KindMemberAck, "event", "central", "shared", "uid", 1, 1, time.Now(), MemberAck{MemberID: "member"}) + if err != nil { + t.Fatal(err) + } + current.APIVersion = "internal.dns.networking.miloapis.com/v1alpha1" + if current.Validate() == nil { + t.Fatal("obsolete transport version accepted") + } +} diff --git a/internal/internaldns/model/private_records.go b/internal/internaldns/model/private_records.go new file mode 100644 index 00000000..e0a6d9af --- /dev/null +++ b/internal/internaldns/model/private_records.go @@ -0,0 +1,22 @@ +package model + +import ( + "fmt" + "strings" +) + +// ValidatePrivateRecordOwner reserves the authority metadata generated by the +// regional BIND fleet. It applies only to internal DNS publication inputs. +func ValidatePrivateRecordOwner(owner, apex, recordType string) error { + owner = strings.TrimSpace(owner) + if strings.EqualFold(recordType, "ALIAS") { + return fmt.Errorf("private DNS does not support ALIAS flattening") + } + if strings.EqualFold(recordType, "SOA") { + return fmt.Errorf("private zone SOA records are managed by the DNS service") + } + if strings.EqualFold(recordType, "CNAME") && (owner == "" || owner == "@" || AbsoluteName(owner) == AbsoluteName(apex)) { + return fmt.Errorf("a private zone apex cannot be a CNAME") + } + return nil +} diff --git a/internal/internaldns/model/snapshot_keys.go b/internal/internaldns/model/snapshot_keys.go new file mode 100644 index 00000000..9a6214d4 --- /dev/null +++ b/internal/internaldns/model/snapshot_keys.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package model + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "strings" +) + +func SnapshotPrefix(region, shard string) string { + return SafeToken(region) + "." + SafeToken(shard) + "." +} + +func PublicationActivationName(manifest, region, shard string) string { + name := manifest + "-" + SafeToken(region) + "-" + SafeToken(shard) + "-activate" + if len(name) <= 63 { + return strings.Trim(name, "-") + } + sum := sha256.Sum256([]byte(name)) + return strings.Trim(name[:50], "-") + fmt.Sprintf("-%x", sum[:6]) +} + +// Chunk keys retain a complete immutable revision until its manifest head has +// moved and the supported replay window has elapsed. A manifest head is written +// only after every referenced chunk is durable in both JetStream and bootstrap. +func SnapshotKey(env Envelope) (string, error) { + prefix := SnapshotPrefix(env.Region, env.Shard) + uid := OpaqueToken(env.ResourceUID) + switch env.Kind { + case KindPublicationChunk: + var c PublicationChunk + if err := json.Unmarshal(env.Payload, &c); err != nil { + return "", err + } + return fmt.Sprintf("%schunk.%s.e%d.r%d.i%d", prefix, uid, env.Epoch, env.Revision, c.Index), nil + case KindPublicationManifest: + return prefix + "manifest." + uid, nil + case KindServingSnapshot: + return prefix + "serving." + uid, nil + default: + return "", fmt.Errorf("event %s is not bootstrap state", env.Kind) + } +} diff --git a/internal/internaldns/model/subjects.go b/internal/internaldns/model/subjects.go new file mode 100644 index 00000000..783f0db0 --- /dev/null +++ b/internal/internaldns/model/subjects.go @@ -0,0 +1,23 @@ +package model + +import "fmt" + +func ServingSubject(region, shard string) string { + return fmt.Sprintf("dns.private.serving.%s.%s", SafeToken(region), SafeToken(shard)) +} + +func RecordChunkSubject(region, shard, zoneUID string) string { + return fmt.Sprintf("dns.private.records.%s.%s.%s.chunk", SafeToken(region), SafeToken(shard), OpaqueToken(zoneUID)) +} + +func RecordManifestSubject(region, shard, zoneUID string) string { + return fmt.Sprintf("dns.private.records.%s.%s.%s.manifest", SafeToken(region), SafeToken(shard), OpaqueToken(zoneUID)) +} + +func RecordConsumerFilter(region, shard string) string { + return fmt.Sprintf("dns.private.records.%s.%s.>", SafeToken(region), SafeToken(shard)) +} + +func AckSubject(region, shard, memberID string) string { + return fmt.Sprintf("dns.private.acks.%s.%s.%s", SafeToken(region), SafeToken(shard), OpaqueToken(memberID)) +} From 2fc1638f3a65603380d8b7b24ced34db1f49e490 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 19:11:00 -0500 Subject: [PATCH 02/28] refactor: Keep DNS changes ready for review --- internal/internaldns/admission/handler_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 23fc815f..9bfee9f7 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -15,7 +15,6 @@ import ( apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client/fake" kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -123,7 +122,7 @@ func request(resource, username string, obj, old any) kubeadmission.Request { return kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ UID: types.UID("request-uid"), Namespace: "project", Operation: admissionv1.Update, Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: resource}, - Kind: metav1.GroupVersionKind{Group: schema.GroupVersion(dnsv1alpha1.GroupVersion).Group, Version: "v1alpha1"}, + Kind: metav1.GroupVersionKind{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1"}, UserInfo: authenticationv1.UserInfo{Username: username}, Object: runtime.RawExtension{Raw: raw}, OldObject: runtime.RawExtension{Raw: oldRaw}, }} } From 7c2f1fa8b2cea9d6f6caa7713ae4e21f52a6566a Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 20:11:18 -0500 Subject: [PATCH 03/28] fix: Pin private zone lifetimes and bound dynamic TTLs --- api/v1alpha1/internaldns_project_types.go | 21 ++++++++--- ...orking.miloapis.com_dnsnamingpolicies.yaml | 7 ++++ ...working.miloapis.com_dnsregistrations.yaml | 7 ++++ ...king.miloapis.com_dnszoneassociations.yaml | 2 + internal/internaldns/admission/handler.go | 37 ++++++++++++++++--- .../internaldns/admission/handler_test.go | 25 ++++++++++++- 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index ddc481df..e61221e3 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -21,6 +21,7 @@ type DNSObjectReference struct { // +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" // +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" +// +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSZoneAssociationSpec struct { DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` VPCRef DNSObjectReference `json:"vpcRef,omitempty"` @@ -173,13 +174,19 @@ const ( DNSPublicationPolicyPersistent DNSPublicationPolicy = "Persistent" ) +const DNSRegistrationMaxTTLSeconds int32 = 30 + +// +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSRegistrationSpec struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - Name string `json:"name"` - RecordTypes []RRType `json:"recordTypes"` - PublicationPolicy DNSPublicationPolicy `json:"publicationPolicy"` - TTLSeconds int32 `json:"ttlSeconds"` - ReservedDescendants []string `json:"reservedDescendants,omitempty"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + Name string `json:"name"` + RecordTypes []RRType `json:"recordTypes"` + PublicationPolicy DNSPublicationPolicy `json:"publicationPolicy"` + // TTLSeconds bounds cached dynamic answers to at most 30 seconds after withdrawal. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=30 + TTLSeconds int32 `json:"ttlSeconds"` + ReservedDescendants []string `json:"reservedDescendants,omitempty"` } type DNSRegistrationStatus struct { @@ -286,6 +293,7 @@ type DNSRecordContributionList struct { // +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" // +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" +// +kubebuilder:validation:XValidation:rule="!has(self.dnsZoneRef) || self.dnsZoneRef.name.size() == 0 || (has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0)",message="a selected dnsZoneRef must pin the zone UID" type DNSNamingPolicySpec struct { VPCRef DNSObjectReference `json:"vpcRef,omitempty"` ResolverContextRef DNSObjectReference `json:"resolverContextRef,omitempty"` @@ -307,6 +315,7 @@ const ( DNSRegistrationClassServiceExport DNSRegistrationClass = "ServiceExport" ) +// +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSAdditionalNameRule struct { RegistrationClass DNSRegistrationClass `json:"registrationClass"` DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml index 0c9f59ed..895042f1 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -73,6 +73,10 @@ spec: - namePrefix - registrationClass type: object + x-kubernetes-validations: + - message: dnsZoneRef must pin the zone UID + rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > + 0 type: array dnsZoneRef: description: Legacy single-rule fields are retained during the v1alpha1 @@ -158,6 +162,9 @@ spec: || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))' + - message: a selected dnsZoneRef must pin the zone UID + rule: '!has(self.dnsZoneRef) || self.dnsZoneRef.name.size() == 0 || + (has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0)' status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml index d216c964..117680ee 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -88,7 +88,11 @@ spec: type: string type: array ttlSeconds: + description: TTLSeconds bounds cached dynamic answers to at most 30 + seconds after withdrawal. format: int32 + maximum: 30 + minimum: 1 type: integer required: - dnsZoneRef @@ -97,6 +101,9 @@ spec: - recordTypes - ttlSeconds type: object + x-kubernetes-validations: + - message: dnsZoneRef must pin the zone UID + rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 status: properties: canonicalFQDN: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml index eccb6edd..067a8319 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml @@ -118,6 +118,8 @@ spec: || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))' + - message: dnsZoneRef must pin the zone UID + rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 status: properties: bindingRef: diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index ba6592bb..066e7949 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -368,6 +368,9 @@ func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadm if err := decode(req, &a); err != nil { return err } + if _, err := currentPrivateZone(ctx, cl, req.Namespace, a.Spec.DNSZoneRef); err != nil { + return err + } if a.Spec.Managed { return fmt.Errorf("managed associations are platform-owned") } @@ -395,11 +398,17 @@ func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubead return err } if p.Spec.DNSZoneRef.Name != "" { + if _, err := currentPrivateZone(ctx, cl, req.Namespace, p.Spec.DNSZoneRef); err != nil { + return err + } if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", p.Spec.DNSZoneRef.Name, "update"); err != nil { return err } } for _, rule := range p.Spec.AdditionalNames { + if _, err := currentPrivateZone(ctx, cl, req.Namespace, rule.DNSZoneRef); err != nil { + return err + } if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", rule.DNSZoneRef.Name, "update"); err != nil { return err } @@ -506,12 +515,12 @@ func (h *Handler) registration(ctx context.Context, cl client.Client, req kubead if err := decode(req, ®istration); err != nil { return err } - var zone dnsv1alpha1.DNSZone - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: registration.Spec.DNSZoneRef.Name}, &zone); err != nil { - return fmt.Errorf("private zone lookup: %w", err) + if registration.Spec.TTLSeconds < 1 || registration.Spec.TTLSeconds > dnsv1alpha1.DNSRegistrationMaxTTLSeconds { + return fmt.Errorf("dynamic TTL must be between 1 and %d seconds", dnsv1alpha1.DNSRegistrationMaxTTLSeconds) } - if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { - return fmt.Errorf("internal registrations require a private zone") + zone, err := currentPrivateZone(ctx, cl, req.Namespace, registration.Spec.DNSZoneRef) + if err != nil { + return err } for _, rt := range registration.Spec.RecordTypes { if err := model.ValidatePrivateRecordOwner(registration.Spec.Name, zone.Spec.DomainName, string(rt)); err != nil { @@ -520,3 +529,21 @@ func (h *Handler) registration(ctx context.Context, cl client.Client, req kubead } return nil } + +// Zone references cannot silently follow a replacement with the same name. +func currentPrivateZone(ctx context.Context, cl client.Client, namespace string, ref dnsv1alpha1.DNSObjectReference) (*dnsv1alpha1.DNSZone, error) { + if ref.Name == "" || ref.UID == "" { + return nil, fmt.Errorf("zone reference must pin name and UID") + } + var zone dnsv1alpha1.DNSZone + if err := cl.Get(ctx, client.ObjectKey{Namespace: namespace, Name: ref.Name}, &zone); err != nil { + return nil, fmt.Errorf("private zone lookup: %w", err) + } + if zone.UID != ref.UID || !zone.DeletionTimestamp.IsZero() || (ref.Generation != 0 && ref.Generation != zone.Generation) { + return nil, fmt.Errorf("zone lifetime is not current") + } + if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { + return nil, fmt.Errorf("internal references require a private zone") + } + return &zone, nil +} diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 9bfee9f7..a4d6a38f 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -245,17 +245,38 @@ func TestPrivateRegistrationRejectsAuthorityOverrides(t *testing.T) { if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } - zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project", UID: "zone-uid"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone).Build()} for _, tt := range []struct { owner string rt dnsv1alpha1.RRType allowed bool }{{"api", dnsv1alpha1.RRTypeA, true}, {"api", dnsv1alpha1.RRTypeCNAME, true}, {"@", dnsv1alpha1.RRTypeCNAME, false}, {"CORP.INTERNAL.", dnsv1alpha1.RRTypeCNAME, false}, {"@", dnsv1alpha1.RRTypeSOA, false}} { - registration := dnsv1alpha1.DNSRegistration{Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name}, Name: tt.owner, RecordTypes: []dnsv1alpha1.RRType{tt.rt}}} + registration := dnsv1alpha1.DNSRegistration{Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: zone.UID}, TTLSeconds: 5, Name: tt.owner, RecordTypes: []dnsv1alpha1.RRType{tt.rt}}} response := h.Handle(context.Background(), request("dnsregistrations", "tenant", registration, nil)) if response.Allowed != tt.allowed { t.Fatalf("owner %s type %s allowed=%v expected=%v", tt.owner, tt.rt, response.Allowed, tt.allowed) } } } + +func TestRegistrationRequiresCurrentZoneAndBoundedTTL(t *testing.T) { + scheme := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project", UID: "current"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone).Build()} + for _, tt := range []struct { + uid types.UID + ttl int32 + allowed bool + }{ + {"current", 1, true}, {"current", 30, true}, {"", 5, false}, {"retired", 5, false}, {"current", 0, false}, {"current", 31, false}, {"current", 2147483647, false}, + } { + reg := dnsv1alpha1.DNSRegistration{Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: tt.uid}, TTLSeconds: tt.ttl, Name: "api", RecordTypes: []dnsv1alpha1.RRType{dnsv1alpha1.RRTypeA}}} + if got := h.Handle(context.Background(), request("dnsregistrations", "tenant", reg, nil)); got.Allowed != tt.allowed { + t.Errorf("UID=%q TTL=%d: allowed=%v: %v", tt.uid, tt.ttl, got.Allowed, got.Result) + } + } +} From d999c3ae7986a34d1709bf0d0ee69b78447a551c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:00:34 -0500 Subject: [PATCH 04/28] Install private DNS CRDs and validate stacked pull requests --- .github/workflows/e2e.yml | 2 -- config/crd/kustomization.yaml | 26 +++++++++----------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4dc79259..91c613e5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -5,8 +5,6 @@ on: branches: - main pull_request: - branches: - - main jobs: e2e: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 0cebf89f..b6efcd04 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,30 +6,22 @@ resources: - bases/dns.networking.miloapis.com_dnszones.yaml - bases/dns.networking.miloapis.com_dnsrecordsets.yaml - bases/dns.networking.miloapis.com_dnszonediscoveries.yaml +- bases/dns.networking.miloapis.com_dnscontributiongrants.yaml +- bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml +- bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +- bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +- bases/dns.networking.miloapis.com_dnsregistrations.yaml +- bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +- bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +- bases/dns.networking.miloapis.com_dnszoneassociations.yaml # +kubebuilder:scaffold:crdkustomizeresource -patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD +#patches: # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. #configurations: #- kustomizeconfig.yaml - -- bases/dns.networking.miloapis.com_dnscontributiongrants.yaml - -- bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml - -- bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml - -- bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml - -- bases/dns.networking.miloapis.com_dnsregistrations.yaml - -- bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml - -- bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml - -- bases/dns.networking.miloapis.com_dnszoneassociations.yaml From a9fc71041732626ce39b7e213861b6b9f3908f68 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:05:37 -0500 Subject: [PATCH 05/28] Validate private DNS API contracts with schemas and CEL --- api/v1alpha1/internaldns_project_types.go | 46 ++++-- .../internaldns_validation_envtest_test.go | 146 ++++++++++++++++++ ...ng.miloapis.com_dnscontributiongrants.yaml | 9 ++ ...ing.miloapis.com_dnsmanagednamespaces.yaml | 9 ++ ...orking.miloapis.com_dnsnamingpolicies.yaml | 24 +++ ...g.miloapis.com_dnsrecordcontributions.yaml | 17 ++ ...working.miloapis.com_dnsregistrations.yaml | 3 + ...iloapis.com_dnsresolveraccessbindings.yaml | 32 ++++ ...king.miloapis.com_dnsresolvercontexts.yaml | 5 + ...king.miloapis.com_dnszoneassociations.yaml | 21 +++ internal/internaldns/admission/handler.go | 68 +------- .../internaldns/admission/handler_test.go | 44 +----- 12 files changed, 306 insertions(+), 118 deletions(-) create mode 100644 api/v1alpha1/internaldns_validation_envtest_test.go diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index e61221e3..04bbd19c 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -13,9 +13,13 @@ import ( // DNSObjectReference pins a namespaced object by API-assigned UID. Generation // is used where a writer must also be fenced against policy changes. type DNSObjectReference struct { - Name string `json:"name"` - UID types.UID `json:"uid,omitempty"` - Generation int64 `json:"generation,omitempty"` + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MaxLength=128 + UID types.UID `json:"uid,omitempty"` + // +kubebuilder:validation:Minimum=0 + Generation int64 `json:"generation,omitempty"` } // +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" @@ -41,6 +45,8 @@ type DNSZoneAssociationStatus struct { // a trusted network integration. consumerID is opaque to DNS and immutable. // +kubebuilder:validation:XValidation:rule="oldSelf.consumerID == self.consumerID",message="consumerID is immutable" type DNSResolverContextSpec struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 ConsumerID string `json:"consumerID"` ManagedNamespace DNSResolverContextManagedNamespace `json:"managedNamespace,omitempty"` } @@ -89,8 +95,10 @@ type DNSResolverQueryIdentityType string const DNSResolverQueryIdentityDestinationAddress DNSResolverQueryIdentityType = "DestinationAddress" type DNSResolverQueryIdentity struct { - Type DNSResolverQueryIdentityType `json:"type"` - Value string `json:"value"` + Type DNSResolverQueryIdentityType `json:"type"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Value string `json:"value"` } type DNSResolverAccessAuthorization struct { @@ -117,11 +125,23 @@ func (a DNSResolverAccessAuthorization) MarshalJSON() ([]byte, error) { }{WriterEpoch: a.WriterEpoch, Sequence: a.Sequence, ValidUntil: deadline}) } +// +kubebuilder:validation:XValidation:rule="self.contextRef.name.size() > 0 && has(self.contextRef.uid) && self.contextRef.uid.size() > 0",message="contextRef requires name and UID" +// +kubebuilder:validation:XValidation:rule="self.contextRef == oldSelf.contextRef",message="contextRef is immutable" +// +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity && self.port == oldSelf.port && self.transports == oldSelf.transports",message="access identity and transport are immutable" +// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch == oldSelf.authorization.writerEpoch",message="writerEpoch is immutable for an access binding" +// +kubebuilder:validation:XValidation:rule="self.authorization == oldSelf.authorization || self.authorization.sequence > oldSelf.authorization.sequence",message="authorization changes require an increasing sequence" type DNSResolverAccessBindingSpec struct { - ContextRef DNSObjectReference `json:"contextRef"` - Region string `json:"region"` - QueryIdentity DNSResolverQueryIdentity `json:"queryIdentity"` - Port int32 `json:"port"` + ContextRef DNSObjectReference `json:"contextRef"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Region string `json:"region"` + QueryIdentity DNSResolverQueryIdentity `json:"queryIdentity"` + // +kubebuilder:validation:Enum=53 + Port int32 `json:"port"` + // +kubebuilder:validation:MinItems=2 + // +kubebuilder:validation:MaxItems=2 + // +kubebuilder:validation:items:Enum=UDP;TCP + // +listType=set Transports []string `json:"transports"` Authorization DNSResolverAccessAuthorization `json:"authorization"` } @@ -215,10 +235,13 @@ type DNSRegistrationList struct { } type DNSProducerPrincipal struct { + // +kubebuilder:validation:MinLength=1 ClusterUID string `json:"clusterUID"` - Subject string `json:"subject"` + // +kubebuilder:validation:MinLength=1 + Subject string `json:"subject"` } +// +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0",message="registrationRef requires name and UID" type DNSContributionGrantSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` ProducerID string `json:"producerID"` @@ -256,6 +279,9 @@ type DNSContributionRecordSet struct { Records []RecordEntry `json:"records"` } +// +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0",message="registrationRef requires name, UID, and generation" +// +kubebuilder:validation:XValidation:rule="self.grantRef.name.size() > 0 && has(self.grantRef.uid) && self.grantRef.uid.size() > 0",message="grantRef requires name and UID" +// +kubebuilder:validation:XValidation:rule="self.registrationRef == oldSelf.registrationRef && self.grantRef == oldSelf.grantRef",message="contribution authority references are immutable" type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` GrantRef DNSObjectReference `json:"grantRef"` diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go new file mode 100644 index 00000000..a4f9578e --- /dev/null +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +// These tests exercise the served CRD schemas without an admission webhook. +func TestPrivateDNSValidationAtAPIServer(t *testing.T) { + assets := os.Getenv("KUBEBUILDER_ASSETS") + if assets == "" { + t.Skip("set KUBEBUILDER_ASSETS to test private DNS schemas against the API server") + } + environment := &envtest.Environment{BinaryAssetsDirectory: assets, CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true} + cfg, err := environment.Start() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := environment.Stop(); err != nil { + t.Error(err) + } + }) + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + cl, err := client.New(cfg, client.Options{Scheme: scheme}) + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + if err := cl.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "validation"}}); err != nil { + t.Fatal(err) + } + requireInvalid := func(t *testing.T, err error) { + t.Helper() + if !apierrors.IsInvalid(err) { + t.Fatalf("expected API validation rejection, got %v", err) + } + } + newAccess := func(name string) *DNSResolverAccessBinding { + return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ + ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 1, ValidUntil: metav1.NewTime(time.Now().Add(time.Minute))}, + }} + } + for _, tc := range []struct { + name string + mutate func(*DNSResolverAccessBinding) + }{ + {"empty-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "" }}, + {"missing-context-uid", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.UID = "" }}, + {"empty-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "" }}, + {"empty-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "" }}, + {"unsupported-identity", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Type = "SourceAddress" }}, + {"wrong-port", func(a *DNSResolverAccessBinding) { a.Spec.Port = 5353 }}, + {"missing-tcp", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP"} }}, + {"duplicate-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "UDP"} }}, + {"unsupported-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "TLS"} }}, + {"zero-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.WriterEpoch = 0 }}, + {"zero-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.Sequence = 0 }}, + } { + t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name); tc.mutate(a); requireInvalid(t, cl.Create(ctx, a)) }) + } + for _, tc := range []struct { + name string + mutate func(*DNSResolverAccessBinding) + }{ + {"change-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "other" }}, + {"change-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "east" }}, + {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "fd53::2" }}, + {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.WriterEpoch++ }}, + {"extend-without-sequence", func(a *DNSResolverAccessBinding) { + a.Spec.Authorization.ValidUntil = metav1.NewTime(a.Spec.Authorization.ValidUntil.Add(time.Minute)) + }}, + {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.Sequence = 1 }}, + } { + t.Run(tc.name, func(t *testing.T) { + a := newAccess(tc.name) + a.Spec.Authorization.Sequence = 2 + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + tc.mutate(a) + requireInvalid(t, cl.Update(ctx, a)) + }) + } + t.Run("renewal-and-status", func(t *testing.T) { + a := newAccess("renewal") + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.Authorization.Sequence++ + a.Spec.Authorization.ValidUntil = metav1.NewTime(a.Spec.Authorization.ValidUntil.Add(time.Minute)) + if err := cl.Update(ctx, a); err != nil { + t.Fatal(err) + } + a.Status.ObservedSequence = a.Spec.Authorization.Sequence + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatalf("unchanged source authorization blocked status update: %v", err) + } + a.Labels = map[string]string{"test": "metadata"} + if err := cl.Update(ctx, a); err != nil { + t.Fatal(err) + } + }) + t.Run("consumer-identity", func(t *testing.T) { + a := &DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "consumer", Namespace: "validation"}, Spec: DNSResolverContextSpec{ConsumerID: ""}} + requireInvalid(t, cl.Create(ctx, a)) + a.Spec.ConsumerID = "network/application" + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.ConsumerID = "network/other" + requireInvalid(t, cl.Update(ctx, a)) + }) + for _, ttl := range []int32{0, 31, 2147483647} { + t.Run("ttl", func(t *testing.T) { + a := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, PublicationPolicy: DNSPublicationPolicyEligibleContributions, TTLSeconds: ttl}} + requireInvalid(t, cl.Create(ctx, a)) + }) + } + t.Run("contribution-authority", func(t *testing.T) { + a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "contribution", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, GrantRef: DNSObjectReference{Name: "grant", UID: "grant-uid"}, RecordSets: []DNSContributionRecordSet{}}} + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.GrantRef.Name = "other" + requireInvalid(t, cl.Update(ctx, a)) + }) +} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml index d33fde46..46a238eb 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml @@ -45,8 +45,10 @@ spec: principal: properties: clusterUID: + minLength: 1 type: string subject: + minLength: 1 type: string required: - clusterUID @@ -80,14 +82,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -98,6 +103,10 @@ spec: - recordTypes - registrationRef type: object + x-kubernetes-validations: + - message: registrationRef requires name and UID + rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) + && self.registrationRef.uid.size() > 0 status: properties: activeWriterEpoch: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml index 9e56dcaf..5b4c9ecc 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml @@ -58,14 +58,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -85,14 +88,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -162,14 +168,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml index 895042f1..c5df3cd2 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -47,14 +47,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -84,14 +87,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -108,14 +114,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -129,14 +138,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -233,14 +245,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -267,14 +282,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -286,14 +304,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -305,14 +326,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 7d2eafee..163f509d 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -45,14 +45,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -307,14 +310,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -324,6 +330,17 @@ spec: - recordSets - registrationRef type: object + x-kubernetes-validations: + - message: registrationRef requires name, UID, and generation + rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) + && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) + && self.registrationRef.generation > 0 + - message: grantRef requires name and UID + rule: self.grantRef.name.size() > 0 && has(self.grantRef.uid) && self.grantRef.uid.size() + > 0 + - message: contribution authority references are immutable + rule: self.registrationRef == oldSelf.registrationRef && self.grantRef + == oldSelf.grantRef status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml index 117680ee..701b0e8a 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -45,14 +45,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 8436f7dc..99cd47bf 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -63,19 +63,24 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name type: object port: + enum: + - 53 format: int32 type: integer queryIdentity: @@ -85,17 +90,27 @@ spec: - DestinationAddress type: string value: + maxLength: 253 + minLength: 1 type: string required: - type - value type: object region: + maxLength: 253 + minLength: 1 type: string transports: items: + enum: + - UDP + - TCP type: string + maxItems: 2 + minItems: 2 type: array + x-kubernetes-list-type: set required: - authorization - contextRef @@ -104,6 +119,20 @@ spec: - region - transports type: object + x-kubernetes-validations: + - message: contextRef requires name and UID + rule: self.contextRef.name.size() > 0 && has(self.contextRef.uid) && + self.contextRef.uid.size() > 0 + - message: contextRef is immutable + rule: self.contextRef == oldSelf.contextRef + - message: access identity and transport are immutable + rule: self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity + && self.port == oldSelf.port && self.transports == oldSelf.transports + - message: writerEpoch is immutable for an access binding + rule: self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + - message: authorization changes require an increasing sequence + rule: self.authorization == oldSelf.authorization || self.authorization.sequence + > oldSelf.authorization.sequence status: properties: bindingRef: @@ -113,14 +142,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml index f61da29a..0adba855 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -42,6 +42,8 @@ spec: a trusted network integration. consumerID is opaque to DNS and immutable. properties: consumerID: + maxLength: 512 + minLength: 1 type: string managedNamespace: properties: @@ -124,14 +126,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml index 067a8319..8cbd7304 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml @@ -45,14 +45,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -66,14 +69,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -85,14 +91,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -129,14 +138,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -204,14 +216,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -223,14 +238,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -242,14 +260,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 066e7949..e342e4d3 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -153,16 +153,11 @@ func (h *Handler) resolverContext(req kubeadmission.Request) error { if err := decode(req, ¤t); err != nil { return err } - if current.Spec.ConsumerID == "" { - return fmt.Errorf("consumerID is required") - } if len(req.OldObject.Raw) > 0 { if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { return err } - if old.Spec.ConsumerID != current.Spec.ConsumerID { - return fmt.Errorf("consumerID is immutable") - } + if !reflect.DeepEqual(old.Status, current.Status) { return fmt.Errorf("resolver context status is DNS platform-owned") } @@ -176,9 +171,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube return err } a := current.Spec.Authorization - if current.Spec.ContextRef.Name == "" || current.Spec.ContextRef.UID == "" { - return fmt.Errorf("access binding must pin context name and UID") - } var resolverContext dnsv1alpha1.DNSResolverContext if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: current.Spec.ContextRef.Name}, &resolverContext); err != nil { return fmt.Errorf("resolver context lookup: %w", err) @@ -193,15 +185,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { return fmt.Errorf("resolver context is not ready for its current generation") } - if current.Spec.QueryIdentity.Type != dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress { - return fmt.Errorf("query identity type must be DestinationAddress") - } - if current.Spec.Region == "" || current.Spec.QueryIdentity.Value == "" || current.Spec.Port < 1 || a.WriterEpoch < 1 || a.Sequence < 1 || a.ValidUntil.IsZero() { - return fmt.Errorf("region, destination, port, writer epoch, sequence, and deadline are required") - } - if current.Spec.Port != 53 || !validTransports(current.Spec.Transports) { - return fmt.Errorf("access requires port 53 and exactly UDP and TCP") - } max := h.MaxAccessLease if max <= 0 { max = 5 * time.Minute @@ -220,15 +203,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { return err } - if old.Spec.ContextRef != current.Spec.ContextRef || old.Spec.Region != current.Spec.Region || old.Spec.QueryIdentity != current.Spec.QueryIdentity || old.Spec.Port != current.Spec.Port || !reflect.DeepEqual(old.Spec.Transports, current.Spec.Transports) { - return fmt.Errorf("access identity and transport are immutable; create a new binding") - } - if a.WriterEpoch != old.Spec.Authorization.WriterEpoch { - return fmt.Errorf("writer epoch is immutable for an access binding lifetime") - } - if a.Sequence <= old.Spec.Authorization.Sequence { - return fmt.Errorf("access renewal sequence must increase") - } if !reflect.DeepEqual(old.Status, current.Status) { return fmt.Errorf("access binding status is DNS platform-owned") } @@ -236,28 +210,11 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube return nil } -func validTransports(values []string) bool { - if len(values) != 2 { - return false - } - seen := map[string]bool{} - for _, v := range values { - if (v != "UDP" && v != "TCP") || seen[v] { - return false - } - seen[v] = true - } - return seen["UDP"] && seen["TCP"] -} - func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID string, req kubeadmission.Request) error { var c, old dnsv1alpha1.DNSRecordContribution if err := decode(req, &c); err != nil { return err } - if c.Spec.GrantRef.UID == "" || c.Spec.RegistrationRef.UID == "" || c.Spec.RegistrationRef.Generation < 1 { - return fmt.Errorf("contribution must pin grant UID and registration UID/generation") - } var g dnsv1alpha1.DNSContributionGrant if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.GrantRef.Name}, &g); err != nil { return fmt.Errorf("grant lookup: %w", err) @@ -302,9 +259,6 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { return err } - if old.Spec.GrantRef != c.Spec.GrantRef || old.Spec.RegistrationRef != c.Spec.RegistrationRef { - return fmt.Errorf("contribution authority references are immutable; create a new contribution for a new policy lifetime") - } } if req.SubResource == "status" { if !reflect.DeepEqual(old.Status.Conditions, c.Status.Conditions) || old.Status.PublishedRevision != c.Status.PublishedRevision { @@ -347,12 +301,6 @@ func (h *Handler) grant(ctx context.Context, cl client.Client, req kubeadmission if err := decode(req, &g); err != nil { return err } - if g.Spec.Principal.Subject == "" || g.Spec.Principal.ClusterUID == "" { - return fmt.Errorf("producer principal is required") - } - if g.Spec.RegistrationRef.UID == "" { - return fmt.Errorf("grant must pin its registration UID") - } var r dnsv1alpha1.DNSRegistration if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: g.Spec.RegistrationRef.Name}, &r); err != nil { return err @@ -433,16 +381,6 @@ func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubead func validateConsumerSelector(vpcRef, contextRef dnsv1alpha1.DNSObjectReference, contextMode bool) error { hasVPC := vpcRef.Name != "" || vpcRef.UID != "" || vpcRef.Generation != 0 hasContext := contextRef.Name != "" || contextRef.UID != "" || contextRef.Generation != 0 - if hasVPC == hasContext { - return fmt.Errorf("exactly one of vpcRef or resolverContextRef is required") - } - selected := vpcRef - if hasContext { - selected = contextRef - } - if selected.Name == "" || selected.UID == "" { - return fmt.Errorf("consumer reference must pin name and UID") - } if contextMode && !hasContext { return fmt.Errorf("resolver context mode requires resolverContextRef") } @@ -515,9 +453,7 @@ func (h *Handler) registration(ctx context.Context, cl client.Client, req kubead if err := decode(req, ®istration); err != nil { return err } - if registration.Spec.TTLSeconds < 1 || registration.Spec.TTLSeconds > dnsv1alpha1.DNSRegistrationMaxTTLSeconds { - return fmt.Errorf("dynamic TTL must be between 1 and %d seconds", dnsv1alpha1.DNSRegistrationMaxTTLSeconds) - } + zone, err := currentPrivateZone(ctx, cl, req.Namespace, registration.Spec.DNSZoneRef) if err != nil { return err diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index a4d6a38f..4a8c6839 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -5,7 +5,6 @@ package admission import ( "context" "encoding/json" - "fmt" "testing" "time" @@ -176,11 +175,6 @@ func TestResolverAccessRequiresIntegrationAndMonotonicBoundedLease(t *testing.T) t.Fatal("publisher granted network access") } req.UserInfo.Username = h.IntegrationSubjects[0] - replay := old.DeepCopy() - req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], replay, old) - if h.Handle(context.Background(), req).Allowed { - t.Fatal("replayed sequence accepted") - } overlong := current.DeepCopy() overlong.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(6 * time.Minute)) req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], overlong, old) @@ -202,40 +196,6 @@ func TestResolverAccessRequiresIntegrationAndMonotonicBoundedLease(t *testing.T) } } -func TestConsumerSelectorRejectsBothOrEmptyInEveryMode(t *testing.T) { - vpc := dnsv1alpha1.DNSObjectReference{Name: "vpc", UID: "vpc-uid"} - resolverContext := dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"} - for _, contextMode := range []bool{false, true} { - for _, tc := range []struct { - name string - vpc, context dnsv1alpha1.DNSObjectReference - }{{"empty", dnsv1alpha1.DNSObjectReference{}, dnsv1alpha1.DNSObjectReference{}}, {"both", vpc, resolverContext}, {"unpinned vpc", dnsv1alpha1.DNSObjectReference{Name: "vpc"}, dnsv1alpha1.DNSObjectReference{}}, {"unpinned context", dnsv1alpha1.DNSObjectReference{}, dnsv1alpha1.DNSObjectReference{Name: "ctx"}}} { - t.Run(fmt.Sprintf("context=%v/%s", contextMode, tc.name), func(t *testing.T) { - if err := validateConsumerSelector(tc.vpc, tc.context, contextMode); err == nil { - t.Fatal("ambiguous or unpinned selector accepted") - } - }) - } - } - for _, tc := range []struct { - name string - vpc, context dnsv1alpha1.DNSObjectReference - }{{"complete vpc partial context", vpc, dnsv1alpha1.DNSObjectReference{Name: "ctx"}}, {"partial vpc complete context", dnsv1alpha1.DNSObjectReference{UID: "vpc-uid"}, resolverContext}} { - if err := validateConsumerSelector(tc.vpc, tc.context, true); err == nil { - t.Fatalf("%s accepted in context mode", tc.name) - } - if err := validateConsumerSelector(tc.vpc, tc.context, false); err == nil { - t.Fatalf("%s accepted in legacy mode", tc.name) - } - } - if err := validateConsumerSelector(vpc, dnsv1alpha1.DNSObjectReference{}, false); err != nil { - t.Fatalf("legacy selector rejected: %v", err) - } - if err := validateConsumerSelector(dnsv1alpha1.DNSObjectReference{}, resolverContext, true); err != nil { - t.Fatalf("context selector rejected: %v", err) - } -} - func setAdmissionCondition(conditions *[]metav1.Condition, conditionType string, status metav1.ConditionStatus, generation int64, now time.Time) { apimeta.SetStatusCondition(conditions, metav1.Condition{Type: conditionType, Status: status, Reason: "Test", ObservedGeneration: generation, LastTransitionTime: metav1.NewTime(now)}) } @@ -260,7 +220,7 @@ func TestPrivateRegistrationRejectsAuthorityOverrides(t *testing.T) { } } -func TestRegistrationRequiresCurrentZoneAndBoundedTTL(t *testing.T) { +func TestRegistrationRequiresCurrentZone(t *testing.T) { scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) @@ -272,7 +232,7 @@ func TestRegistrationRequiresCurrentZoneAndBoundedTTL(t *testing.T) { ttl int32 allowed bool }{ - {"current", 1, true}, {"current", 30, true}, {"", 5, false}, {"retired", 5, false}, {"current", 0, false}, {"current", 31, false}, {"current", 2147483647, false}, + {"current", 1, true}, {"current", 30, true}, {"", 5, false}, {"retired", 5, false}, } { reg := dnsv1alpha1.DNSRegistration{Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: tt.uid}, TTLSeconds: tt.ttl, Name: "api", RecordTypes: []dnsv1alpha1.RRType{dnsv1alpha1.RRTypeA}}} if got := h.Handle(context.Background(), request("dnsregistrations", "tenant", reg, nil)); got.Allowed != tt.allowed { From 3858c290a858da014f7a6bf6eb7d9414f1af92ff Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:34:43 -0500 Subject: [PATCH 06/28] Authorize private DNS resource access through API server policies --- .../iam/protected-resources/internal-dns.yaml | 125 ++++++++++++ .../protected-resources/kustomization.yaml | 1 + config/iam/roles/dns-admin.yaml | 12 ++ config/iam/roles/dns-viewer.yaml | 21 ++ internal/internaldns/admission/handler.go | 190 +++--------------- .../internaldns/admission/handler_test.go | 70 ++++--- 6 files changed, 232 insertions(+), 187 deletions(-) create mode 100644 config/iam/protected-resources/internal-dns.yaml diff --git a/config/iam/protected-resources/internal-dns.yaml b/config/iam/protected-resources/internal-dns.yaml new file mode 100644 index 00000000..6c289ef9 --- /dev/null +++ b/config/iam/protected-resources/internal-dns.yaml @@ -0,0 +1,125 @@ +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: dns.networking.miloapis.com-dnszoneassociation +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSZoneAssociation + plural: dnszoneassociations + singular: dnszoneassociation + 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-dnsnamingpolicie +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSNamingPolicy + plural: dnsnamingpolicies + singular: dnsnamingpolicie + 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-dnsregistration +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSRegistration + plural: dnsregistrations + singular: dnsregistration + 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-dnscontributiongrant +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSContributionGrant + plural: dnscontributiongrants + singular: dnscontributiongrant + 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-dnsrecordcontribution +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSRecordContribution + plural: dnsrecordcontributions + singular: dnsrecordcontribution + 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-dnsresolvercontext +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSResolverContext + plural: dnsresolvercontexts + singular: dnsresolvercontext + 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-dnsresolveraccessbinding +spec: + serviceRef: + name: dns.networking.miloapis.com + kind: DNSResolverAccessBinding + plural: dnsresolveraccessbindings + singular: dnsresolveraccessbinding + permissions: [list, get, watch, create, update, patch, delete] + subresources: + - name: status + permissions: [get, update, patch] + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project diff --git a/config/iam/protected-resources/kustomization.yaml b/config/iam/protected-resources/kustomization.yaml index 2df1df9c..c5b52617 100644 --- a/config/iam/protected-resources/kustomization.yaml +++ b/config/iam/protected-resources/kustomization.yaml @@ -10,3 +10,4 @@ resources: - dnsrecordsets.yaml - dnszoneclasses.yaml - dnszonediscoveries.yaml + - internal-dns.yaml diff --git a/config/iam/roles/dns-admin.yaml b/config/iam/roles/dns-admin.yaml index a01343f2..c0c89858 100644 --- a/config/iam/roles/dns-admin.yaml +++ b/config/iam/roles/dns-admin.yaml @@ -24,3 +24,15 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.delete + - dns.networking.miloapis.com/dnszoneassociations.create + - dns.networking.miloapis.com/dnszoneassociations.update + - dns.networking.miloapis.com/dnszoneassociations.patch + - dns.networking.miloapis.com/dnszoneassociations.delete + - dns.networking.miloapis.com/dnsnamingpolicies.create + - dns.networking.miloapis.com/dnsnamingpolicies.update + - dns.networking.miloapis.com/dnsnamingpolicies.patch + - dns.networking.miloapis.com/dnsnamingpolicies.delete + - dns.networking.miloapis.com/dnsregistrations.create + - dns.networking.miloapis.com/dnsregistrations.update + - dns.networking.miloapis.com/dnsregistrations.patch + - dns.networking.miloapis.com/dnsregistrations.delete diff --git a/config/iam/roles/dns-viewer.yaml b/config/iam/roles/dns-viewer.yaml index 97e2de49..3d1fec00 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -22,3 +22,24 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.watch + - dns.networking.miloapis.com/dnszoneassociations.list + - dns.networking.miloapis.com/dnszoneassociations.get + - dns.networking.miloapis.com/dnszoneassociations.watch + - dns.networking.miloapis.com/dnsnamingpolicies.list + - dns.networking.miloapis.com/dnsnamingpolicies.get + - dns.networking.miloapis.com/dnsnamingpolicies.watch + - dns.networking.miloapis.com/dnsregistrations.list + - dns.networking.miloapis.com/dnsregistrations.get + - dns.networking.miloapis.com/dnsregistrations.watch + - dns.networking.miloapis.com/dnscontributiongrants.list + - dns.networking.miloapis.com/dnscontributiongrants.get + - dns.networking.miloapis.com/dnscontributiongrants.watch + - dns.networking.miloapis.com/dnsrecordcontributions.list + - dns.networking.miloapis.com/dnsrecordcontributions.get + - dns.networking.miloapis.com/dnsrecordcontributions.watch + - dns.networking.miloapis.com/dnsresolvercontexts.list + - dns.networking.miloapis.com/dnsresolvercontexts.get + - dns.networking.miloapis.com/dnsresolvercontexts.watch + - dns.networking.miloapis.com/dnsresolveraccessbindings.list + - dns.networking.miloapis.com/dnsresolveraccessbindings.get + - dns.networking.miloapis.com/dnsresolveraccessbindings.watch diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index e342e4d3..7d67ae1c 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only -// Package admission enforces the authenticated writer boundary for internal DNS. +// Package admission validates live DNS references and publisher grant relationships. // Compiler validation remains necessary, but cannot infer a Kubernetes caller // from a stored object's spec. Install this webhook with failurePolicy: Fail. package admission @@ -16,8 +16,6 @@ import ( dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" "go.miloapis.com/dns-operator/internal/internaldns/model" admissionv1 "k8s.io/api/admission/v1" - authenticationv1 "k8s.io/api/authentication/v1" - authorizationv1 "k8s.io/api/authorization/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,8 +31,6 @@ const Path = "/validate-internal-dns" type Handler struct { Client client.Client TrustedClusterUID string - PlatformSubjects []string - IntegrationSubjects []string MaxContributionLease time.Duration MaxAccessLease time.Duration ResolverContextsEnabled bool @@ -48,73 +44,18 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm if req.Resource.Group != dnsv1alpha1.GroupVersion.Group { return kubeadmission.Denied("unexpected API group") } - // Public-zone admission remains owned by the existing DNS operator. This - // webhook must not give the internal service exclusive authority over the - // public controller's status writes. - if req.Resource.Resource == "dnszones" { - var zone dnsv1alpha1.DNSZone - if err := decode(req, &zone); err != nil { - return kubeadmission.Denied(err.Error()) - } - if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { - return kubeadmission.Allowed("public DNS admission applies") - } - } - platform := contains(h.PlatformSubjects, req.UserInfo.Username) - integration := contains(h.IntegrationSubjects, req.UserInfo.Username) - if req.Resource.Resource == "dnsresolvercontexts" || req.Resource.Resource == "dnsresolveraccessbindings" { - if req.SubResource == "status" { - if !platform { - return kubeadmission.Denied("resolver context status is DNS platform-owned") - } - return kubeadmission.Allowed("platform writer") - } - if !integration { - return kubeadmission.Denied("resolver context and access specs require a trusted integration writer") - } - cl := h.Client - if h.ClientForRequest != nil { - var err error - cl, _, err = h.ClientForRequest(ctx, req) - if err != nil { - return kubeadmission.Errored(http.StatusServiceUnavailable, err) - } - } - if cl == nil { - return kubeadmission.Errored(http.StatusServiceUnavailable, fmt.Errorf("project API client unavailable")) - } - if req.Operation == admissionv1.Delete { - return kubeadmission.Allowed("integration revocation") - } - var err error - if req.Resource.Resource == "dnsresolvercontexts" { - err = h.resolverContext(req) - } else { - err = h.resolverAccess(ctx, cl, req) - } - if err != nil { - return kubeadmission.Denied(err.Error()) - } - return kubeadmission.Allowed("trusted integration writer") - } - if platformResource(req.Resource.Resource) { - if !platform { - return kubeadmission.Denied("internal serving and publication resources require a platform writer") - } - return kubeadmission.Allowed("platform writer") - } - if req.SubResource == "status" && req.Resource.Resource != "dnsrecordcontributions" && !platform { - return kubeadmission.Denied("DNS publication and writer authority status is platform-owned") + // The API server authorizes resource and status access through RBAC. + if req.Operation == admissionv1.Delete { + return kubeadmission.Allowed("API server authorization applies") } - if platform { - return kubeadmission.Allowed("platform writer; compiler validates publication inputs") + if req.SubResource == "status" && req.Resource.Resource != "dnsrecordcontributions" { + return kubeadmission.Allowed("API server status authorization applies") } - // Deletion revokes intent and cannot introduce serving authority. The API - // server has already authorized delete on this exact project resource. - // Requiring a live grant/registration here would strand retired objects and - // block garbage collection after their parents have been deleted. - if req.Operation == admissionv1.Delete { - return kubeadmission.Allowed("project deletion authorization applies") + switch req.Resource.Resource { + case "dnsregistrations", "dnsrecordcontributions", "dnscontributiongrants", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": + // These resources require live references or bounded freshness checks. + default: + return kubeadmission.Allowed("API server authorization and schema validation apply") } cl, clusterUID := h.Client, h.TrustedClusterUID if h.ClientForRequest != nil { @@ -129,6 +70,8 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm } var err error switch req.Resource.Resource { + case "dnsresolveraccessbindings": + err = h.resolverAccess(ctx, cl, req) case "dnsregistrations": err = h.registration(ctx, cl, req) case "dnsrecordcontributions": @@ -148,25 +91,8 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm return kubeadmission.Allowed("authenticated and scoped DNS writer") } -func (h *Handler) resolverContext(req kubeadmission.Request) error { - var current, old dnsv1alpha1.DNSResolverContext - if err := decode(req, ¤t); err != nil { - return err - } - if len(req.OldObject.Raw) > 0 { - if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { - return err - } - - if !reflect.DeepEqual(old.Status, current.Status) { - return fmt.Errorf("resolver context status is DNS platform-owned") - } - } - return nil -} - func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kubeadmission.Request) error { - var current, old dnsv1alpha1.DNSResolverAccessBinding + var current dnsv1alpha1.DNSResolverAccessBinding if err := decode(req, ¤t); err != nil { return err } @@ -199,14 +125,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if a.ValidUntil.After(now.Add(max)) { return fmt.Errorf("access deadline exceeds platform lease limit") } - if len(req.OldObject.Raw) > 0 { - if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { - return err - } - if !reflect.DeepEqual(old.Status, current.Status) { - return fmt.Errorf("access binding status is DNS platform-owned") - } - } return nil } @@ -215,6 +133,19 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if err := decode(req, &c); err != nil { return err } + if len(req.OldObject.Raw) > 0 { + if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { + return err + } + } + if req.SubResource == "status" { + previous, current := old.Status, c.Status + previous.Conditions, current.Conditions = nil, nil + previous.PublishedRevision, current.PublishedRevision = 0, 0 + if reflect.DeepEqual(previous, current) { + return nil + } + } var g dnsv1alpha1.DNSContributionGrant if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.GrantRef.Name}, &g); err != nil { return fmt.Errorf("grant lookup: %w", err) @@ -255,14 +186,9 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID return fmt.Errorf("record type %s is outside the writer's scope", rrset.RecordType) } } - if len(req.OldObject.Raw) > 0 { - if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { - return err - } - } if req.SubResource == "status" { if !reflect.DeepEqual(old.Status.Conditions, c.Status.Conditions) || old.Status.PublishedRevision != c.Status.PublishedRevision { - return fmt.Errorf("DNS publication status fields are platform-owned") + return fmt.Errorf("publisher observations cannot change publication results") } if c.Status.WriterEpoch != g.Status.ActiveWriterEpoch { return fmt.Errorf("writer epoch is retired or not issued") @@ -308,7 +234,7 @@ func (h *Handler) grant(ctx context.Context, cl client.Client, req kubeadmission if r.UID != g.Spec.RegistrationRef.UID { return fmt.Errorf("registration lifetime is not current") } - return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsregistrations", r.Name, "update") + return nil } func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadmission.Request) error { @@ -319,22 +245,16 @@ func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadm if _, err := currentPrivateZone(ctx, cl, req.Namespace, a.Spec.DNSZoneRef); err != nil { return err } - if a.Spec.Managed { - return fmt.Errorf("managed associations are platform-owned") - } if err := validateConsumerSelector(a.Spec.VPCRef, a.Spec.ResolverContextRef, h.ResolverContextsEnabled); err != nil { return err } - if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", a.Spec.DNSZoneRef.Name, "update"); err != nil { - return err - } if h.ResolverContextsEnabled { if a.Spec.ResolverContextRef.UID == "" { return fmt.Errorf("association must pin resolver context UID") } - return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsresolvercontexts", a.Spec.ResolverContextRef.Name, "use") + return nil } - return authorize(ctx, cl, req.UserInfo, req.Namespace, "networking.datumapis.com", "networks", a.Spec.VPCRef.Name, "use") + return nil } func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubeadmission.Request) error { @@ -349,17 +269,11 @@ func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubead if _, err := currentPrivateZone(ctx, cl, req.Namespace, p.Spec.DNSZoneRef); err != nil { return err } - if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", p.Spec.DNSZoneRef.Name, "update"); err != nil { - return err - } } for _, rule := range p.Spec.AdditionalNames { if _, err := currentPrivateZone(ctx, cl, req.Namespace, rule.DNSZoneRef); err != nil { return err } - if err := authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnszones", rule.DNSZoneRef.Name, "update"); err != nil { - return err - } } if h.ResolverContextsEnabled { if p.Spec.ResolverContextRef.Name == "" || p.Spec.ResolverContextRef.UID == "" { @@ -373,9 +287,9 @@ func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubead if resolverContext.UID != p.Spec.ResolverContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() || ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { return fmt.Errorf("resolver context lifetime is not ready") } - return authorize(ctx, cl, req.UserInfo, req.Namespace, dnsv1alpha1.GroupVersion.Group, "dnsresolvercontexts", resolverContext.Name, "use") + return nil } - return authorize(ctx, cl, req.UserInfo, req.Namespace, "networking.datumapis.com", "networks", p.Spec.VPCRef.Name, "use") + return nil } func validateConsumerSelector(vpcRef, contextRef dnsv1alpha1.DNSObjectReference, contextMode bool) error { @@ -390,27 +304,6 @@ func validateConsumerSelector(vpcRef, contextRef dnsv1alpha1.DNSObjectReference, return nil } -func authorize(ctx context.Context, cl client.Client, user authenticationv1.UserInfo, namespace, group, resource, name, verb string) error { - if name == "" { - return fmt.Errorf("authorization target name is required") - } - extra := map[string]authorizationv1.ExtraValue{} - for k, v := range user.Extra { - extra[k] = authorizationv1.ExtraValue(v) - } - review := &authorizationv1.SubjectAccessReview{ObjectMeta: metav1.ObjectMeta{}, Spec: authorizationv1.SubjectAccessReviewSpec{ - User: user.Username, UID: user.UID, Groups: user.Groups, Extra: extra, - ResourceAttributes: &authorizationv1.ResourceAttributes{Namespace: namespace, Group: group, Resource: resource, Name: name, Verb: verb}, - }} - if err := cl.Create(ctx, review); err != nil { - return fmt.Errorf("authorization review unavailable: %w", err) - } - if !review.Status.Allowed { - return fmt.Errorf("caller is not authorized to %s %s/%s", verb, resource, name) - } - return nil -} - func decode(req kubeadmission.Request, obj any) error { data := req.Object.Raw if req.Operation == admissionv1.Delete { @@ -422,23 +315,6 @@ func decode(req kubeadmission.Request, obj any) error { return json.Unmarshal(data, obj) } -func platformResource(resource string) bool { - switch resource { - case "dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes", "dnspublicationownerships", "dnsmanagednamespaces": - return true - default: - return false - } -} - -func contains(values []string, value string) bool { - for _, v := range values { - if v == value { - return true - } - } - return false -} func containsType(values []dnsv1alpha1.RRType, value dnsv1alpha1.RRType) bool { for _, v := range values { if v == value { diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 4a8c6839..d1fb1c31 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -57,7 +57,8 @@ func TestContributionAuthenticatedWriter(t *testing.T) { {"scoped fresh producer", producer, nil, true}, {"another producer", "system:serviceaccount:connector-system:dns-publisher", nil, false}, {"retired epoch", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 7 }, false}, - {"replayed sequence", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 17 }, false}, + {"unchanged observation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 17 }, true}, + {"decreasing sequence", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 16 }, false}, {"stale generation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ObservedGeneration = 1 }, false}, {"forged grant UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.GrantRef.UID = "other-grant" }, false}, {"retired registration UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.RegistrationRef.UID = "old-reg" }, false}, @@ -83,22 +84,17 @@ func TestContributionAuthenticatedWriter(t *testing.T) { } } -func TestPlatformAuthorityCannotBeWrittenByProducer(t *testing.T) { - h := Handler{PlatformSubjects: []string{"system:serviceaccount:dns-system:control-plane"}} - for _, resource := range []string{"dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes"} { - req := request(resource, "system:serviceaccount:compute-system:dns-publisher", map[string]any{}, nil) - if h.Handle(context.Background(), req).Allowed { - t.Fatalf("producer allowed to write %s", resource) - } - req.UserInfo.Username = h.PlatformSubjects[0] - if !h.Handle(context.Background(), req).Allowed { - t.Fatalf("platform writer denied %s", resource) +func TestResourceAccessUsesAPIServerAuthorization(t *testing.T) { + h := Handler{} + for _, resource := range []string{"dnsresolvercontexts", "dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes", "dnspublicationownerships"} { + if !h.Handle(context.Background(), request(resource, "authorized-user", map[string]any{}, nil)).Allowed { + t.Fatalf("admission duplicates resource authorization for %s", resource) } } - req := request("dnscontributiongrants", "system:serviceaccount:compute-system:dns-publisher", map[string]any{}, nil) + req := request("dnscontributiongrants", "authorized-user", map[string]any{}, nil) req.SubResource = "status" - if h.Handle(context.Background(), req).Allowed { - t.Fatal("producer allowed to issue own epoch") + if !h.Handle(context.Background(), req).Allowed { + t.Fatal("admission duplicates status authorization") } } @@ -127,7 +123,7 @@ func request(resource, username string, obj, old any) kubeadmission.Request { } func TestInternalAdmissionPreservesPublicZoneControllerStatus(t *testing.T) { - h := Handler{PlatformSubjects: []string{"system:serviceaccount:internal-dns-system:controller"}} + h := Handler{} req := kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Update, Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: "dnszones"}, SubResource: "status", UserInfo: authenticationv1.UserInfo{Username: "system:serviceaccount:dns-system:public-controller"}}} zone := dnsv1alpha1.DNSZone{Spec: dnsv1alpha1.DNSZoneSpec{Visibility: dnsv1alpha1.DNSZoneVisibilityPublic}} req.Object.Raw, _ = json.Marshal(zone) @@ -136,8 +132,8 @@ func TestInternalAdmissionPreservesPublicZoneControllerStatus(t *testing.T) { } zone.Spec.Visibility = dnsv1alpha1.DNSZoneVisibilityPrivate req.Object.Raw, _ = json.Marshal(zone) - if h.Handle(context.Background(), req).Allowed { - t.Fatal("legacy public writer received private publication authority") + if !h.Handle(context.Background(), req).Allowed { + t.Fatal("status authorization must use the API server") } } @@ -148,12 +144,12 @@ func TestRetiredProductIntentCanBeDeletedWithoutLiveGrant(t *testing.T) { t.Fatal("retired contribution cleanup required a live deleted grant") } req.Resource.Resource = "dnspublicationownerships" - if h.Handle(context.Background(), req).Allowed { - t.Fatal("product cleanup acquired platform ownership deletion authority") + if !h.Handle(context.Background(), req).Allowed { + t.Fatal("deletion authorization must use the API server") } } -func TestResolverAccessRequiresIntegrationAndMonotonicBoundedLease(t *testing.T) { +func TestResolverAccessRequiresCurrentContextAndBoundedLease(t *testing.T) { now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { @@ -161,36 +157,31 @@ func TestResolverAccessRequiresIntegrationAndMonotonicBoundedLease(t *testing.T) } resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessWriterEpoch: 3}} setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) - h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build(), IntegrationSubjects: []string{"system:serviceaccount:network-system:integration"}, MaxAccessLease: 5 * time.Minute, Now: func() time.Time { return now }} + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build(), MaxAccessLease: 5 * time.Minute, Now: func() time.Time { return now }} old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: dnsv1alpha1.DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 9, ValidUntil: metav1.NewTime(now.Add(2 * time.Minute))}}} current := old.DeepCopy() current.Spec.Authorization.Sequence = 10 current.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(3 * time.Minute)) - req := request("dnsresolveraccessbindings", h.IntegrationSubjects[0], current, old) + req := request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if res := h.Handle(context.Background(), req); !res.Allowed { t.Fatalf("fresh integration renewal denied: %v", res.Result) } - req.UserInfo.Username = "system:serviceaccount:compute:publisher" - if h.Handle(context.Background(), req).Allowed { - t.Fatal("publisher granted network access") - } - req.UserInfo.Username = h.IntegrationSubjects[0] overlong := current.DeepCopy() overlong.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(6 * time.Minute)) - req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], overlong, old) + req = request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", overlong, old) if h.Handle(context.Background(), req).Allowed { t.Fatal("overlong access lease accepted") } forged := current.DeepCopy() forged.Spec.ContextRef.UID = "replacement" - req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], forged, old) + req = request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", forged, old) if h.Handle(context.Background(), req).Allowed { t.Fatal("context lifetime changed in place") } staleContext := resolverContext.DeepCopy() staleContext.Generation = 2 h.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(staleContext).Build() - req = request("dnsresolveraccessbindings", h.IntegrationSubjects[0], current, old) + req = request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if h.Handle(context.Background(), req).Allowed { t.Fatal("stale context Ready condition authorized access") } @@ -240,3 +231,22 @@ func TestRegistrationRequiresCurrentZone(t *testing.T) { } } } + +func TestPublicationStatusDoesNotRenewPublisherObservation(t *testing.T) { + h := Handler{Client: fake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()} + old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 4, Sequence: 10, Eligible: true, ValidUntil: ptrTime(time.Now().Add(-time.Minute))}} + current := old.DeepCopy() + current.Status.PublishedRevision = 12 + current.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue}} + req := request("dnsrecordcontributions", "rbac-authorized-controller", current, old) + req.SubResource = "status" + if result := h.Handle(context.Background(), req); !result.Allowed { + t.Fatalf("publication result needs no producer grant: %v", result.Result) + } + current.Status.ValidUntil = ptrTime(time.Now().Add(time.Minute)) + req = request("dnsrecordcontributions", "rbac-authorized-controller", current, old) + req.SubResource = "status" + if h.Handle(context.Background(), req).Allowed { + t.Fatal("publication update renewed producer freshness without a grant") + } +} From 36aecda377189a5f08b1e19bcf59a65bbd664d67 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:38:30 -0500 Subject: [PATCH 07/28] Keep private DNS admission constants and roles consistent --- config/iam/roles/dns-admin.yaml | 2 -- config/iam/roles/dns-viewer.yaml | 2 -- internal/internaldns/admission/handler.go | 8 +++++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/config/iam/roles/dns-admin.yaml b/config/iam/roles/dns-admin.yaml index c0c89858..a5a01d5c 100644 --- a/config/iam/roles/dns-admin.yaml +++ b/config/iam/roles/dns-admin.yaml @@ -22,8 +22,6 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.update - dns.networking.miloapis.com/dnszonediscoveries.patch - dns.networking.miloapis.com/dnszonediscoveries.delete - - - dns.networking.miloapis.com/dnszoneassociations.create - dns.networking.miloapis.com/dnszoneassociations.update - dns.networking.miloapis.com/dnszoneassociations.patch diff --git a/config/iam/roles/dns-viewer.yaml b/config/iam/roles/dns-viewer.yaml index 3d1fec00..a4549fef 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -20,8 +20,6 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.list - dns.networking.miloapis.com/dnszonediscoveries.get - dns.networking.miloapis.com/dnszonediscoveries.watch - - - dns.networking.miloapis.com/dnszoneassociations.list - dns.networking.miloapis.com/dnszoneassociations.get - dns.networking.miloapis.com/dnszoneassociations.watch diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 7d67ae1c..57f9c533 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -22,6 +22,8 @@ import ( kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) +const contributionResource = "dnsrecordcontributions" + const Path = "/validate-internal-dns" // Handler uses an uncached project client for grant and registration lookups. @@ -48,11 +50,11 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm if req.Operation == admissionv1.Delete { return kubeadmission.Allowed("API server authorization applies") } - if req.SubResource == "status" && req.Resource.Resource != "dnsrecordcontributions" { + if req.SubResource == "status" && req.Resource.Resource != contributionResource { return kubeadmission.Allowed("API server status authorization applies") } switch req.Resource.Resource { - case "dnsregistrations", "dnsrecordcontributions", "dnscontributiongrants", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": + case "dnsregistrations", contributionResource, "dnscontributiongrants", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": // These resources require live references or bounded freshness checks. default: return kubeadmission.Allowed("API server authorization and schema validation apply") @@ -74,7 +76,7 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm err = h.resolverAccess(ctx, cl, req) case "dnsregistrations": err = h.registration(ctx, cl, req) - case "dnsrecordcontributions": + case contributionResource: err = h.contribution(ctx, cl, clusterUID, req) case "dnscontributiongrants": err = h.grant(ctx, cl, req) From 0a6f18d64bccbed71deea72a0504597cdc97d5b7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 17:07:23 -0500 Subject: [PATCH 08/28] Use the DNSNamingPolicy resource name in IAM definitions --- config/iam/protected-resources/internal-dns.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/iam/protected-resources/internal-dns.yaml b/config/iam/protected-resources/internal-dns.yaml index 6c289ef9..a4803eb8 100644 --- a/config/iam/protected-resources/internal-dns.yaml +++ b/config/iam/protected-resources/internal-dns.yaml @@ -19,13 +19,13 @@ spec: apiVersion: iam.miloapis.com/v1alpha1 kind: ProtectedResource metadata: - name: dns.networking.miloapis.com-dnsnamingpolicie + name: dns.networking.miloapis.com-dnsnamingpolicy spec: serviceRef: name: dns.networking.miloapis.com kind: DNSNamingPolicy plural: dnsnamingpolicies - singular: dnsnamingpolicie + singular: dnsnamingpolicy permissions: [list, get, watch, create, update, patch, delete] subresources: - name: status From 54f39c67e876caef70df40f64ba24b0ed5bfb3f6 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:09:11 -0500 Subject: [PATCH 09/28] feat: Define internal DNS fleet delivery contracts --- api/v1alpha1/internaldns_binding_test.go | 37 + api/v1alpha1/internaldns_service_types.go | 264 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 672 ++++++++++++++++++ ...ing.miloapis.com_dnspublicationchunks.yaml | 74 ++ ....miloapis.com_dnspublicationmanifests.yaml | 293 ++++++++ ...miloapis.com_dnspublicationownerships.yaml | 75 ++ ...king.miloapis.com_dnsresolverbindings.yaml | 378 ++++++++++ ...ing.miloapis.com_dnstransportoutboxes.yaml | 139 ++++ config/crd/kustomization.yaml | 5 + go.mod | 5 +- go.sum | 52 +- internal/internaldns/transport/jetstream.go | 434 +++++++++++ .../internaldns/transport/jetstream_test.go | 55 ++ 13 files changed, 2440 insertions(+), 43 deletions(-) create mode 100644 api/v1alpha1/internaldns_binding_test.go create mode 100644 api/v1alpha1/internaldns_service_types.go create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml create mode 100644 config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml create mode 100644 internal/internaldns/transport/jetstream.go create mode 100644 internal/internaldns/transport/jetstream_test.go diff --git a/api/v1alpha1/internaldns_binding_test.go b/api/v1alpha1/internaldns_binding_test.go new file mode 100644 index 00000000..ce16887b --- /dev/null +++ b/api/v1alpha1/internaldns_binding_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "encoding/json" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestResolverBindingUsesNestedContractAndIndependentCopies(t *testing.T) { + original := &DNSResolverBinding{Spec: DNSResolverBindingSpec{ + Source: DNSResolverBindingSource{ProjectUID: "project", ResolverContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, AccessBindingRef: DNSObjectReference{Name: "access", UID: "access-uid"}}, + Placement: DNSResolverBindingPlacement{Region: "central", Shard: "shared"}, + Configuration: DNSResolverBindingConfiguration{Generation: 1, Revision: 2, Listeners: DNSResolverBindingListeners{Node: DNSResolverBindingListener{Address: "fd53::1", Port: 53, Transports: []string{"UDP", "TCP"}}, Regional: DNSResolverBindingListener{Address: "fd54::1", Port: 53, Transports: []string{"UDP", "TCP"}}}, ZoneRefs: []DNSObjectReference{{Name: "private", UID: "zone-uid"}}}, + Authorization: DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 4, ValidUntil: metav1.Now()}, + }} + copy := original.DeepCopy() + copy.Spec.Configuration.ZoneRefs[0].Name = "changed" + copy.Spec.Configuration.Listeners.Node.Transports[0] = "changed" + copy.Spec.Configuration.Listeners.Regional.Transports[0] = "changed" + if original.Spec.Configuration.ZoneRefs[0].Name != "private" || original.Spec.Configuration.Listeners.Node.Transports[0] != "UDP" || original.Spec.Configuration.Listeners.Regional.Transports[0] != "UDP" { + t.Fatal("deep copy aliases nested API collections") + } + raw, err := json.Marshal(original.Spec) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err = json.Unmarshal(raw, &fields); err != nil { + t.Fatal(err) + } + if len(fields) != 4 || fields["source"] == nil || fields["placement"] == nil || fields["configuration"] == nil || fields["authorization"] == nil { + t.Fatalf("unexpected binding wire structure: %s", raw) + } +} diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go new file mode 100644 index 00000000..b55071d0 --- /dev/null +++ b/api/v1alpha1/internaldns_service_types.go @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// DNSResolverBinding is the DNS-service projection of a consumer context and access binding. +// +kubebuilder:validation:XValidation:rule="self.configuration.generation >= oldSelf.configuration.generation && self.configuration.revision >= oldSelf.configuration.revision",message="configuration fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence >= oldSelf.authorization.sequence)",message="authorization fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch != oldSelf.authorization.writerEpoch || self.authorization.sequence != oldSelf.authorization.sequence || self.authorization.validUntil == oldSelf.authorization.validUntil",message="changing an authorization deadline requires a new fence" +// +kubebuilder:validation:XValidation:rule="self.configuration.zoneRefs == oldSelf.configuration.zoneRefs || (self.configuration.generation > oldSelf.configuration.generation && self.configuration.revision > oldSelf.configuration.revision)",message="zone membership changes require a new cache generation and configuration revision" +// +kubebuilder:validation:XValidation:rule="(has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision",message="changing withdrawal state requires a new configuration revision" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence > oldSelf.authorization.sequence)",message="reactivating withdrawn access requires a new source authorization fence" +type DNSResolverBindingSpec struct { + Source DNSResolverBindingSource `json:"source"` + Placement DNSResolverBindingPlacement `json:"placement"` + Configuration DNSResolverBindingConfiguration `json:"configuration"` + Authorization DNSResolverAccessAuthorization `json:"authorization"` + Tombstone bool `json:"tombstone,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding source identity is immutable" +// +kubebuilder:validation:XValidation:rule="self.projectUID.size() > 0 && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) && self.accessBindingRef.uid.size() > 0",message="binding source requires project UID and UID-pinned context and access references" +type DNSResolverBindingSource struct { + ProjectUID types.UID `json:"projectUID"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` + AccessBindingRef DNSObjectReference `json:"accessBindingRef"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding placement is immutable" +type DNSResolverBindingPlacement struct { + Region string `json:"region"` + Shard string `json:"shard"` +} + +// +kubebuilder:validation:XValidation:rule="self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() > 0)",message="zone references must pin name and UID" +type DNSResolverBindingConfiguration struct { + // +kubebuilder:validation:Minimum=1 + Generation int64 `json:"generation"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + Listeners DNSResolverBindingListeners `json:"listeners"` + // +kubebuilder:validation:MaxItems=256 + ZoneRefs []DNSObjectReference `json:"zoneRefs"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="listener identity is immutable for an access binding" +type DNSResolverBindingListeners struct { + Node DNSResolverBindingListener `json:"node"` + Regional DNSResolverBindingListener `json:"regional"` +} + +type DNSResolverBindingListener struct { + // +kubebuilder:validation:MinLength=1 + Address string `json:"address"` + // +kubebuilder:validation:Enum=53 + Port int32 `json:"port"` + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=2 + // +kubebuilder:validation:items:Enum=UDP;TCP + // +listType=set + Transports []string `json:"transports"` +} + +type DNSApplyAcknowledgement struct { + MemberID string `json:"memberID"` + Phase string `json:"phase"` + Revision int64 `json:"revision"` + WriterEpoch int64 `json:"writerEpoch,omitempty"` + SnapshotRevision int64 `json:"snapshotRevision,omitempty"` + AuthorizationIssuerEpoch int64 `json:"authorizationIssuerEpoch,omitempty"` + AuthorizationRevision int64 `json:"authorizationRevision,omitempty"` + ObservedUnixNano int64 `json:"observedUnixNano,omitempty"` + ObservedAt metav1.Time `json:"observedAt"` + ValidUntil *metav1.Time `json:"validUntil,omitempty"` + Error string `json:"error,omitempty"` +} + +type DNSResolverBindingStatus struct { + ObservedConfigurationRevision int64 `json:"observedConfigurationRevision,omitempty"` + Phase string `json:"phase,omitempty"` + MemberAcknowledgements []DNSApplyAcknowledgement `json:"memberAcknowledgements,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSResolverBinding struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSResolverBindingSpec `json:"spec"` + Status DNSResolverBindingStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSResolverBindingList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSResolverBinding `json:"items"` +} + +type DNSPublicationChunkReference struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int32 `json:"size"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication manifests are immutable" +type DNSPublicationManifestSpec struct { + ZoneRef DNSObjectReference `json:"zoneRef"` + ZoneApex string `json:"zoneApex"` + ContextUIDs []types.UID `json:"contextUIDs"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + PreviousRevision int64 `json:"previousRevision,omitempty"` + Tombstone bool `json:"tombstone,omitempty"` + Chunks []DNSPublicationChunkReference `json:"chunks"` + ContentHash string `json:"contentHash"` + GeneratedAt metav1.Time `json:"generatedAt"` + ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` + RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` + ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` +} + +type DNSPublicationTarget struct { + Region string `json:"region"` + Shard string `json:"shard"` +} + +type DNSRegistrationFence struct { + UID types.UID `json:"uid"` + Generation int64 `json:"generation"` +} + +// DNSContributionFence persists an authenticated high-water sequence even +// while its observation is unhealthy, rejected, or expired. +type DNSContributionFence struct { + UID types.UID `json:"uid"` + GrantUID types.UID `json:"grantUID"` + Epoch int64 `json:"epoch"` + Sequence int64 `json:"sequence"` + ValidUntil metav1.Time `json:"validUntil,omitempty"` +} + +type DNSPublicationManifestStatus struct { + ExportState string `json:"exportState,omitempty"` + ExportAttempts int32 `json:"exportAttempts,omitempty"` + ExportedAt *metav1.Time `json:"exportedAt,omitempty"` + StreamSequence uint64 `json:"streamSequence,omitempty"` + ReplicaAcknowledgements []DNSApplyAcknowledgement `json:"replicaAcknowledgements,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type DNSPublicationManifest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationManifestSpec `json:"spec"` + Status DNSPublicationManifestStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSPublicationManifestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationManifest `json:"items"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication chunks are immutable" +type DNSPublicationChunkSpec struct { + ManifestUID types.UID `json:"manifestUID,omitempty"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + Index int32 `json:"index"` + SHA256 string `json:"sha256"` + Payload []byte `json:"payload"` +} + +// +kubebuilder:object:root=true +type DNSPublicationChunk struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationChunkSpec `json:"spec"` +} + +// +kubebuilder:object:root=true +type DNSPublicationChunkList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationChunk `json:"items"` +} + +// +kubebuilder:validation:XValidation:rule="oldSelf == self",message="outbox entries are immutable" +type DNSTransportOutboxSpec struct { + Subject string `json:"subject"` + ResourceUID types.UID `json:"resourceUID"` + WriterEpoch int64 `json:"writerEpoch"` + Revision int64 `json:"revision"` + ManifestRef DNSObjectReference `json:"manifestRef"` + OwnershipRef DNSObjectReference `json:"ownershipRef,omitempty"` + PayloadHash string `json:"payloadHash"` + Payload []byte `json:"payload"` + Activation bool `json:"activation,omitempty"` + DependsOn []string `json:"dependsOn,omitempty"` +} + +type DNSTransportOutboxStatus struct { + State string `json:"state,omitempty"` + Attempts int32 `json:"attempts,omitempty"` + AcknowledgedAt *metav1.Time `json:"acknowledgedAt,omitempty"` + StreamSequence uint64 `json:"streamSequence,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:selectablefield:JSONPath=".status.state" +type DNSTransportOutbox struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSTransportOutboxSpec `json:"spec"` + Status DNSTransportOutboxStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSTransportOutboxList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSTransportOutbox `json:"items"` +} + +// DNSPublicationOwnership is a durable compare-and-swap record for a zone +// compiler. A lease takeover increments WriterEpoch and fences delayed holders. +type DNSPublicationOwnershipSpec struct { + ZoneUID types.UID `json:"zoneUID"` + HolderIdentity string `json:"holderIdentity"` + WriterEpoch int64 `json:"writerEpoch"` + NextRevision int64 `json:"nextRevision"` + LeaseUntil metav1.Time `json:"leaseUntil"` + LastContentHash string `json:"lastContentHash,omitempty"` + ActiveManifestName string `json:"activeManifestName,omitempty"` +} + +// +kubebuilder:object:root=true +type DNSPublicationOwnership struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DNSPublicationOwnershipSpec `json:"spec"` +} + +// +kubebuilder:object:root=true +type DNSPublicationOwnershipList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DNSPublicationOwnership `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DNSResolverBinding{}, &DNSResolverBindingList{}, &DNSPublicationManifest{}, &DNSPublicationManifestList{}, &DNSPublicationChunk{}, &DNSPublicationChunkList{}, &DNSTransportOutbox{}, &DNSTransportOutboxList{}, &DNSPublicationOwnership{}, &DNSPublicationOwnershipList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9abcf217..aed412fe 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -10,6 +10,7 @@ import ( "go.datum.net/network-services-operator/api/v1alpha" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -103,6 +104,42 @@ func (in *DNSAdditionalNameRule) DeepCopy() *DNSAdditionalNameRule { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSApplyAcknowledgement) DeepCopyInto(out *DNSApplyAcknowledgement) { + *out = *in + in.ObservedAt.DeepCopyInto(&out.ObservedAt) + if in.ValidUntil != nil { + in, out := &in.ValidUntil, &out.ValidUntil + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSApplyAcknowledgement. +func (in *DNSApplyAcknowledgement) DeepCopy() *DNSApplyAcknowledgement { + if in == nil { + return nil + } + out := new(DNSApplyAcknowledgement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSContributionFence) DeepCopyInto(out *DNSContributionFence) { + *out = *in + in.ValidUntil.DeepCopyInto(&out.ValidUntil) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionFence. +func (in *DNSContributionFence) DeepCopy() *DNSContributionFence { + if in == nil { + return nil + } + out := new(DNSContributionFence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSContributionGrant) DeepCopyInto(out *DNSContributionGrant) { *out = *in @@ -474,6 +511,324 @@ func (in *DNSProducerPrincipal) DeepCopy() *DNSProducerPrincipal { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunk) DeepCopyInto(out *DNSPublicationChunk) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunk. +func (in *DNSPublicationChunk) DeepCopy() *DNSPublicationChunk { + if in == nil { + return nil + } + out := new(DNSPublicationChunk) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationChunk) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkList) DeepCopyInto(out *DNSPublicationChunkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationChunk, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkList. +func (in *DNSPublicationChunkList) DeepCopy() *DNSPublicationChunkList { + if in == nil { + return nil + } + out := new(DNSPublicationChunkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationChunkList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkReference) DeepCopyInto(out *DNSPublicationChunkReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkReference. +func (in *DNSPublicationChunkReference) DeepCopy() *DNSPublicationChunkReference { + if in == nil { + return nil + } + out := new(DNSPublicationChunkReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationChunkSpec) DeepCopyInto(out *DNSPublicationChunkSpec) { + *out = *in + if in.Payload != nil { + in, out := &in.Payload, &out.Payload + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationChunkSpec. +func (in *DNSPublicationChunkSpec) DeepCopy() *DNSPublicationChunkSpec { + if in == nil { + return nil + } + out := new(DNSPublicationChunkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifest) DeepCopyInto(out *DNSPublicationManifest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifest. +func (in *DNSPublicationManifest) DeepCopy() *DNSPublicationManifest { + if in == nil { + return nil + } + out := new(DNSPublicationManifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationManifest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestList) DeepCopyInto(out *DNSPublicationManifestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationManifest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestList. +func (in *DNSPublicationManifestList) DeepCopy() *DNSPublicationManifestList { + if in == nil { + return nil + } + out := new(DNSPublicationManifestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationManifestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestSpec) DeepCopyInto(out *DNSPublicationManifestSpec) { + *out = *in + out.ZoneRef = in.ZoneRef + if in.ContextUIDs != nil { + in, out := &in.ContextUIDs, &out.ContextUIDs + *out = make([]types.UID, len(*in)) + copy(*out, *in) + } + if in.Chunks != nil { + in, out := &in.Chunks, &out.Chunks + *out = make([]DNSPublicationChunkReference, len(*in)) + copy(*out, *in) + } + in.GeneratedAt.DeepCopyInto(&out.GeneratedAt) + if in.ContributionFences != nil { + in, out := &in.ContributionFences, &out.ContributionFences + *out = make([]DNSContributionFence, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RegistrationFences != nil { + in, out := &in.RegistrationFences, &out.RegistrationFences + *out = make([]DNSRegistrationFence, len(*in)) + copy(*out, *in) + } + if in.ServingTargets != nil { + in, out := &in.ServingTargets, &out.ServingTargets + *out = make([]DNSPublicationTarget, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestSpec. +func (in *DNSPublicationManifestSpec) DeepCopy() *DNSPublicationManifestSpec { + if in == nil { + return nil + } + out := new(DNSPublicationManifestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationManifestStatus) DeepCopyInto(out *DNSPublicationManifestStatus) { + *out = *in + if in.ExportedAt != nil { + in, out := &in.ExportedAt, &out.ExportedAt + *out = (*in).DeepCopy() + } + if in.ReplicaAcknowledgements != nil { + in, out := &in.ReplicaAcknowledgements, &out.ReplicaAcknowledgements + *out = make([]DNSApplyAcknowledgement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationManifestStatus. +func (in *DNSPublicationManifestStatus) DeepCopy() *DNSPublicationManifestStatus { + if in == nil { + return nil + } + out := new(DNSPublicationManifestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnership) DeepCopyInto(out *DNSPublicationOwnership) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnership. +func (in *DNSPublicationOwnership) DeepCopy() *DNSPublicationOwnership { + if in == nil { + return nil + } + out := new(DNSPublicationOwnership) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationOwnership) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnershipList) DeepCopyInto(out *DNSPublicationOwnershipList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSPublicationOwnership, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnershipList. +func (in *DNSPublicationOwnershipList) DeepCopy() *DNSPublicationOwnershipList { + if in == nil { + return nil + } + out := new(DNSPublicationOwnershipList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSPublicationOwnershipList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationOwnershipSpec) DeepCopyInto(out *DNSPublicationOwnershipSpec) { + *out = *in + in.LeaseUntil.DeepCopyInto(&out.LeaseUntil) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationOwnershipSpec. +func (in *DNSPublicationOwnershipSpec) DeepCopy() *DNSPublicationOwnershipSpec { + if in == nil { + return nil + } + out := new(DNSPublicationOwnershipSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSPublicationTarget) DeepCopyInto(out *DNSPublicationTarget) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSPublicationTarget. +func (in *DNSPublicationTarget) DeepCopy() *DNSPublicationTarget { + if in == nil { + return nil + } + out := new(DNSPublicationTarget) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordContribution) DeepCopyInto(out *DNSRecordContribution) { *out = *in @@ -721,6 +1076,21 @@ func (in *DNSRegistration) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistrationFence) DeepCopyInto(out *DNSRegistrationFence) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationFence. +func (in *DNSRegistrationFence) DeepCopy() *DNSRegistrationFence { + if in == nil { + return nil + } + out := new(DNSRegistrationFence) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRegistrationList) DeepCopyInto(out *DNSRegistrationList) { *out = *in @@ -942,6 +1312,203 @@ func (in *DNSResolverAccessBindingStatus) DeepCopy() *DNSResolverAccessBindingSt return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBinding) DeepCopyInto(out *DNSResolverBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBinding. +func (in *DNSResolverBinding) DeepCopy() *DNSResolverBinding { + if in == nil { + return nil + } + out := new(DNSResolverBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverBinding) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingConfiguration) DeepCopyInto(out *DNSResolverBindingConfiguration) { + *out = *in + in.Listeners.DeepCopyInto(&out.Listeners) + if in.ZoneRefs != nil { + in, out := &in.ZoneRefs, &out.ZoneRefs + *out = make([]DNSObjectReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingConfiguration. +func (in *DNSResolverBindingConfiguration) DeepCopy() *DNSResolverBindingConfiguration { + if in == nil { + return nil + } + out := new(DNSResolverBindingConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingList) DeepCopyInto(out *DNSResolverBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSResolverBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingList. +func (in *DNSResolverBindingList) DeepCopy() *DNSResolverBindingList { + if in == nil { + return nil + } + out := new(DNSResolverBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSResolverBindingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingListener) DeepCopyInto(out *DNSResolverBindingListener) { + *out = *in + if in.Transports != nil { + in, out := &in.Transports, &out.Transports + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingListener. +func (in *DNSResolverBindingListener) DeepCopy() *DNSResolverBindingListener { + if in == nil { + return nil + } + out := new(DNSResolverBindingListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingListeners) DeepCopyInto(out *DNSResolverBindingListeners) { + *out = *in + in.Node.DeepCopyInto(&out.Node) + in.Regional.DeepCopyInto(&out.Regional) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingListeners. +func (in *DNSResolverBindingListeners) DeepCopy() *DNSResolverBindingListeners { + if in == nil { + return nil + } + out := new(DNSResolverBindingListeners) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingPlacement) DeepCopyInto(out *DNSResolverBindingPlacement) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingPlacement. +func (in *DNSResolverBindingPlacement) DeepCopy() *DNSResolverBindingPlacement { + if in == nil { + return nil + } + out := new(DNSResolverBindingPlacement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingSource) DeepCopyInto(out *DNSResolverBindingSource) { + *out = *in + out.ResolverContextRef = in.ResolverContextRef + out.AccessBindingRef = in.AccessBindingRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingSource. +func (in *DNSResolverBindingSource) DeepCopy() *DNSResolverBindingSource { + if in == nil { + return nil + } + out := new(DNSResolverBindingSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingSpec) DeepCopyInto(out *DNSResolverBindingSpec) { + *out = *in + out.Source = in.Source + out.Placement = in.Placement + in.Configuration.DeepCopyInto(&out.Configuration) + in.Authorization.DeepCopyInto(&out.Authorization) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingSpec. +func (in *DNSResolverBindingSpec) DeepCopy() *DNSResolverBindingSpec { + if in == nil { + return nil + } + out := new(DNSResolverBindingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverBindingStatus) DeepCopyInto(out *DNSResolverBindingStatus) { + *out = *in + if in.MemberAcknowledgements != nil { + in, out := &in.MemberAcknowledgements, &out.MemberAcknowledgements + *out = make([]DNSApplyAcknowledgement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingStatus. +func (in *DNSResolverBindingStatus) DeepCopy() *DNSResolverBindingStatus { + if in == nil { + return nil + } + out := new(DNSResolverBindingStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSResolverContext) DeepCopyInto(out *DNSResolverContext) { *out = *in @@ -1106,6 +1673,111 @@ func (in *DNSResolverQueryIdentity) DeepCopy() *DNSResolverQueryIdentity { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutbox) DeepCopyInto(out *DNSTransportOutbox) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutbox. +func (in *DNSTransportOutbox) DeepCopy() *DNSTransportOutbox { + if in == nil { + return nil + } + out := new(DNSTransportOutbox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSTransportOutbox) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxList) DeepCopyInto(out *DNSTransportOutboxList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSTransportOutbox, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxList. +func (in *DNSTransportOutboxList) DeepCopy() *DNSTransportOutboxList { + if in == nil { + return nil + } + out := new(DNSTransportOutboxList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSTransportOutboxList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxSpec) DeepCopyInto(out *DNSTransportOutboxSpec) { + *out = *in + out.ManifestRef = in.ManifestRef + out.OwnershipRef = in.OwnershipRef + if in.Payload != nil { + in, out := &in.Payload, &out.Payload + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxSpec. +func (in *DNSTransportOutboxSpec) DeepCopy() *DNSTransportOutboxSpec { + if in == nil { + return nil + } + out := new(DNSTransportOutboxSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSTransportOutboxStatus) DeepCopyInto(out *DNSTransportOutboxStatus) { + *out = *in + if in.AcknowledgedAt != nil { + in, out := &in.AcknowledgedAt, &out.AcknowledgedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSTransportOutboxStatus. +func (in *DNSTransportOutboxStatus) DeepCopy() *DNSTransportOutboxStatus { + if in == nil { + return nil + } + out := new(DNSTransportOutboxStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSZone) DeepCopyInto(out *DNSZone) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml new file mode 100644 index 00000000..72f5fe48 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationchunks.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationChunk + listKind: DNSPublicationChunkList + plural: dnspublicationchunks + singular: dnspublicationchunk + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + index: + format: int32 + type: integer + manifestUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + payload: + format: byte + type: string + revision: + format: int64 + type: integer + sha256: + type: string + writerEpoch: + format: int64 + type: integer + required: + - index + - payload + - revision + - sha256 + - writerEpoch + type: object + x-kubernetes-validations: + - message: publication chunks are immutable + rule: oldSelf == self + required: + - spec + type: object + served: true + storage: true diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml new file mode 100644 index 00000000..80dc8429 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml @@ -0,0 +1,293 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationmanifests.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationManifest + listKind: DNSPublicationManifestList + plural: dnspublicationmanifests + singular: dnspublicationmanifest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + chunks: + items: + properties: + name: + type: string + sha256: + type: string + size: + format: int32 + type: integer + required: + - name + - sha256 + - size + type: object + type: array + contentHash: + type: string + contextUIDs: + items: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + type: array + contributionFences: + items: + description: |- + DNSContributionFence persists an authenticated high-water sequence even + while its observation is unhealthy, rejected, or expired. + properties: + epoch: + format: int64 + type: integer + grantUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + sequence: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + validUntil: + format: date-time + type: string + required: + - epoch + - grantUID + - sequence + - uid + type: object + type: array + generatedAt: + format: date-time + type: string + previousRevision: + format: int64 + type: integer + registrationFences: + items: + properties: + generation: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - generation + - uid + type: object + type: array + revision: + format: int64 + type: integer + servingTargets: + items: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + type: array + tombstone: + type: boolean + writerEpoch: + format: int64 + type: integer + zoneApex: + type: string + zoneRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - chunks + - contentHash + - contextUIDs + - generatedAt + - revision + - writerEpoch + - zoneApex + - zoneRef + type: object + x-kubernetes-validations: + - message: publication manifests are immutable + rule: oldSelf == self + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + exportAttempts: + format: int32 + type: integer + exportState: + type: string + exportedAt: + format: date-time + type: string + replicaAcknowledgements: + items: + properties: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + streamSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml new file mode 100644 index 00000000..2e9ae926 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnspublicationownerships.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSPublicationOwnership + listKind: DNSPublicationOwnershipList + plural: dnspublicationownerships + singular: dnspublicationownership + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + DNSPublicationOwnership is a durable compare-and-swap record for a zone + compiler. A lease takeover increments WriterEpoch and fences delayed holders. + properties: + activeManifestName: + type: string + holderIdentity: + type: string + lastContentHash: + type: string + leaseUntil: + format: date-time + type: string + nextRevision: + format: int64 + type: integer + writerEpoch: + format: int64 + type: integer + zoneUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - holderIdentity + - leaseUntil + - nextRevision + - writerEpoch + - zoneUID + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml new file mode 100644 index 00000000..adab7d82 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -0,0 +1,378 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsresolverbindings.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverBinding + listKind: DNSResolverBindingList + plural: dnsresolverbindings + singular: dnsresolverbinding + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DNSResolverBinding is the DNS-service projection of a consumer + context and access binding. + properties: + authorization: + properties: + sequence: + format: int64 + minimum: 1 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + minimum: 1 + type: integer + required: + - sequence + - validUntil + - writerEpoch + type: object + configuration: + properties: + generation: + format: int64 + minimum: 1 + type: integer + listeners: + properties: + node: + properties: + address: + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + regional: + properties: + address: + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + required: + - node + - regional + type: object + x-kubernetes-validations: + - message: listener identity is immutable for an access binding + rule: oldSelf == self + revision: + format: int64 + minimum: 1 + type: integer + zoneRefs: + items: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + maxItems: 256 + type: array + required: + - generation + - listeners + - revision + - zoneRefs + type: object + x-kubernetes-validations: + - message: zone references must pin name and UID + rule: self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() + > 0) + placement: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + x-kubernetes-validations: + - message: binding placement is immutable + rule: oldSelf == self + source: + properties: + accessBindingRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + projectUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + resolverContextRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + required: + - accessBindingRef + - projectUID + - resolverContextRef + type: object + x-kubernetes-validations: + - message: binding source identity is immutable + rule: oldSelf == self + - message: binding source requires project UID and UID-pinned context + and access references + rule: self.projectUID.size() > 0 && self.resolverContextRef.name.size() + > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() + > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) + && self.accessBindingRef.uid.size() > 0 + tombstone: + type: boolean + required: + - authorization + - configuration + - placement + - source + type: object + x-kubernetes-validations: + - message: configuration fences must not decrease + rule: self.configuration.generation >= oldSelf.configuration.generation + && self.configuration.revision >= oldSelf.configuration.revision + - message: authorization fences must not decrease + rule: self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence >= oldSelf.authorization.sequence) + - message: changing an authorization deadline requires a new fence + rule: self.authorization.writerEpoch != oldSelf.authorization.writerEpoch + || self.authorization.sequence != oldSelf.authorization.sequence || + self.authorization.validUntil == oldSelf.authorization.validUntil + - message: zone membership changes require a new cache generation and + configuration revision + rule: self.configuration.zoneRefs == oldSelf.configuration.zoneRefs + || (self.configuration.generation > oldSelf.configuration.generation + && self.configuration.revision > oldSelf.configuration.revision) + - message: changing withdrawal state requires a new configuration revision + rule: (has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) + && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision + - message: reactivating withdrawn access requires a new source authorization + fence + rule: '!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) + && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence > oldSelf.authorization.sequence)' + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + memberAcknowledgements: + items: + properties: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + observedConfigurationRevision: + format: int64 + type: integer + phase: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml new file mode 100644 index 00000000..dab793c6 --- /dev/null +++ b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml @@ -0,0 +1,139 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnstransportoutboxes.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSTransportOutbox + listKind: DNSTransportOutboxList + plural: dnstransportoutboxes + singular: dnstransportoutbox + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + activation: + type: boolean + dependsOn: + items: + type: string + type: array + manifestRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + ownershipRef: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + type: integer + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + type: object + payload: + format: byte + type: string + payloadHash: + type: string + resourceUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + revision: + format: int64 + type: integer + subject: + type: string + writerEpoch: + format: int64 + type: integer + required: + - manifestRef + - payload + - payloadHash + - resourceUID + - revision + - subject + - writerEpoch + type: object + x-kubernetes-validations: + - message: outbox entries are immutable + rule: oldSelf == self + status: + properties: + acknowledgedAt: + format: date-time + type: string + attempts: + format: int32 + type: integer + lastError: + type: string + state: + type: string + streamSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .status.state + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b6efcd04..760dc379 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,11 @@ resources: - bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml - bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml - bases/dns.networking.miloapis.com_dnszoneassociations.yaml +- bases/dns.networking.miloapis.com_dnspublicationchunks.yaml +- bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml +- bases/dns.networking.miloapis.com_dnspublicationownerships.yaml +- bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +- bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml # +kubebuilder:scaffold:crdkustomizeresource # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/go.mod b/go.mod index ff712e5a..f7a6b5ad 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/klauspost/compress v1.18.7 // indirect + github.com/klauspost/compress v1.20.0 // indirect github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect @@ -67,6 +67,8 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect @@ -123,6 +125,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.54.0 github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.67.5 // indirect diff --git a/go.sum b/go.sum index 078c70a3..f4489ec8 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,6 @@ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6O github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -155,8 +153,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -165,8 +161,8 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -213,6 +209,12 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -301,44 +303,28 @@ go.datum.net/datumctl v0.18.1 h1:MP83cRkJ94aq4lYyOm5dSLU60JNnmR486LIeZnaCw8s= go.datum.net/datumctl v0.18.1/go.mod h1:0L0BBgFfud8ZuV9sbsSmeEBUPoK2q9g4boU5yczF92o= go.datum.net/network-services-operator v0.9.0 h1:D+q4iXFlQTeZxPJBxPW7hDJW9sHLMAiVhUjjJKT2R98= go.datum.net/network-services-operator v0.9.0/go.mod h1:RvP9wWGKSlWlASp1pe/vgvaExm+BsD6hq9XJzElN6dY= -go.miloapis.com/milo v0.31.0 h1:gBZ9NaplTCFhgWUZJ18N38JsFDCVtdKmypwJvffSdUU= -go.miloapis.com/milo v0.31.0/go.mod h1:GKK3afjCwshfZfvhjNe1wp/H45z4m7x5oG/8xbSgU1M= go.miloapis.com/milo v0.35.0 h1:pXXqEm8fHeMOS/qBGw2501ftfI3e4nUA/zMZQqUvlmA= go.miloapis.com/milo v0.35.0/go.mod h1:zMzwGXexUlPlFaZ9/4U+2DDbzPj4xzs9I76Z901g2sE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -355,16 +341,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -373,8 +355,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -384,8 +364,6 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -401,16 +379,12 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -419,8 +393,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -438,12 +410,8 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= diff --git a/internal/internaldns/transport/jetstream.go b/internal/internaldns/transport/jetstream.go new file mode 100644 index 00000000..7aa00976 --- /dev/null +++ b/internal/internaldns/transport/jetstream.go @@ -0,0 +1,434 @@ +// Package transport provides the authenticated, durable regional event bus for +// internal DNS. Every serving member must use a +// different Durable name; sharing it would divide updates between replicas. +package transport + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "go.miloapis.com/dns-operator/internal/internaldns/model" +) + +type TLSConfig struct { + CAFile string `json:"caFile,omitempty"` + CertificateFile string `json:"certificateFile,omitempty"` + KeyFile string `json:"keyFile,omitempty"` + ServerName string `json:"serverName,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` +} + +type Config struct { + URL string `json:"url"` + Name string `json:"name"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + PasswordFile string `json:"passwordFile,omitempty"` + Token string `json:"token,omitempty"` + CredentialsFile string `json:"credentialsFile,omitempty"` + TLS *TLSConfig `json:"tls,omitempty"` + // AllowInsecure is intended for isolated development fixtures only. A + // production caller must configure TLS and an authentication method. + AllowInsecure bool `json:"allowInsecure,omitempty"` + ConnectTimeout time.Duration `json:"connectTimeout,omitempty"` + PublishTimeout time.Duration `json:"publishTimeout,omitempty"` +} + +func (c Config) validate() error { + if c.URL == "" || c.Name == "" { + return errors.New("NATS URL and connection name are required") + } + if !c.AllowInsecure && c.TLS == nil { + return errors.New("NATS TLS is required unless AllowInsecure is explicitly set") + } + if c.Password != "" && c.PasswordFile != "" { + return errors.New("NATS password and password file are mutually exclusive") + } + if (c.Password != "" || c.PasswordFile != "") && c.Username == "" { + return errors.New("NATS username is required with a password source") + } + if c.Username != "" && c.Password == "" && c.PasswordFile == "" { + return errors.New("NATS username requires a password source") + } + if c.AllowInsecure && c.CredentialsFile == "" && c.Token == "" && c.Username == "" { + return nil + } + auth := 0 + if c.CredentialsFile != "" { + auth++ + } + if c.Token != "" { + auth++ + } + if c.Username != "" { + auth++ + } + if auth != 1 { + return errors.New("exactly one NATS authentication method is required") + } + return nil +} + +type JetStream struct { + nc *nats.Conn + js jetstream.JetStream + publishTimeout time.Duration +} + +func Connect(c Config) (*JetStream, error) { + if err := c.validate(); err != nil { + return nil, err + } + opts := []nats.Option{ + nats.Name(c.Name), + nats.Timeout(defaultDuration(c.ConnectTimeout, 10*time.Second)), + // A regional partition must not permanently close a long-running agent. + // Callers keep retrying while local expiry and the watchdog gate serving. + nats.MaxReconnects(-1), + } + if c.CredentialsFile != "" { + opts = append(opts, nats.UserCredentials(c.CredentialsFile)) + } + if c.Token != "" { + opts = append(opts, nats.Token(c.Token)) + } + if c.Username != "" { + password := c.Password + if c.PasswordFile != "" { + value, err := os.ReadFile(c.PasswordFile) + if err != nil { + return nil, fmt.Errorf("read NATS password: %w", err) + } + password = strings.TrimSpace(string(value)) + if password == "" { + return nil, errors.New("NATS password file is empty") + } + } + opts = append(opts, nats.UserInfo(c.Username, password)) + } + if c.TLS != nil { + tlsConfig, err := loadTLS(*c.TLS) + if err != nil { + return nil, err + } + opts = append(opts, nats.Secure(tlsConfig)) + } + nc, err := nats.Connect(c.URL, opts...) + if err != nil { + return nil, fmt.Errorf("connect NATS: %w", err) + } + js, err := jetstream.New(nc) + if err != nil { + nc.Close() + return nil, fmt.Errorf("open JetStream: %w", err) + } + return &JetStream{nc: nc, js: js, publishTimeout: defaultDuration(c.PublishTimeout, 5*time.Second)}, nil +} + +func loadTLS(c TLSConfig) (*tls.Config, error) { + if c.InsecureSkipVerify { + return nil, errors.New("NATS TLS InsecureSkipVerify is forbidden") + } + t := &tls.Config{MinVersion: tls.VersionTLS13, ServerName: c.ServerName} + if c.CAFile != "" { + pem, err := os.ReadFile(c.CAFile) + if err != nil { + return nil, fmt.Errorf("read NATS CA: %w", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + return nil, errors.New("NATS CA file contains no certificates") + } + t.RootCAs = pool + } + if c.CertificateFile != "" || c.KeyFile != "" { + if c.CertificateFile == "" || c.KeyFile == "" { + return nil, errors.New("NATS client certificate and key must be configured together") + } + cert, err := tls.LoadX509KeyPair(c.CertificateFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("load NATS client certificate: %w", err) + } + t.Certificates = []tls.Certificate{cert} + } + return t, nil +} + +func (j *JetStream) Close() error { + if j == nil || j.nc == nil { + return nil + } + if err := j.nc.Drain(); err != nil { + j.nc.Close() + return err + } + return nil +} + +type PublishAck struct { + Stream string + Sequence uint64 + Duplicate bool +} + +// ExportPublisher is implemented by JetStream and used by the durable outbox +// controller. EventID supplies JetStream de-duplication for ambiguous retries. +type ExportPublisher interface { + Publish(context.Context, string, model.Envelope) (PublishAck, error) +} + +func (j *JetStream) Publish(ctx context.Context, subject string, env model.Envelope) (PublishAck, error) { + if err := env.Validate(); err != nil { + return PublishAck{}, err + } + if subject == "" { + return PublishAck{}, errors.New("publish subject is required") + } + if j == nil || j.nc == nil || j.nc.IsClosed() { + return PublishAck{}, nats.ErrConnectionClosed + } + if !j.nc.IsConnected() { + return PublishAck{}, nats.ErrDisconnected + } + b, err := json.Marshal(env) + if err != nil { + return PublishAck{}, err + } + publishCtx, cancel := boundedContext(ctx, defaultDuration(j.publishTimeout, 5*time.Second)) + defer cancel() + ack, err := j.js.Publish(publishCtx, subject, b, jetstream.WithMsgID(brokerMessageID(subject, env.EventID))) + if err != nil { + return PublishAck{}, fmt.Errorf("publish %s: %w", subject, err) + } + return PublishAck{Stream: ack.Stream, Sequence: ack.Sequence, Duplicate: ack.Duplicate}, nil +} + +func brokerMessageID(subject, eventID string) string { + // JetStream de-duplication spans every subject in a stream. Namespace the + // application event ID so an ACK publisher cannot reserve a controller + // publication's ID on its independently authorized subject. + return model.Hash([]byte(subject + "\x00" + eventID)) +} + +func boundedContext(parent context.Context, limit time.Duration) (context.Context, context.CancelFunc) { + if deadline, ok := parent.Deadline(); ok && time.Until(deadline) <= limit { + return context.WithCancel(parent) + } + return context.WithTimeout(parent, limit) +} + +type StreamConfig struct { + Name string `json:"name"` + Subjects []string `json:"subjects,omitempty"` + MaxAge time.Duration `json:"maxAge,omitempty"` + MaxBytes int64 `json:"maxBytes,omitempty"` + Replicas int `json:"replicas,omitempty"` + SnapshotsBucket string `json:"snapshotsBucket"` +} + +func (j *JetStream) EnsureStream(ctx context.Context, c StreamConfig) error { + if c.Name == "" || len(c.Subjects) == 0 { + return errors.New("stream name and subjects are required") + } + _, err := j.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{Name: c.Name, Subjects: c.Subjects, Retention: jetstream.LimitsPolicy, Storage: jetstream.FileStorage, MaxAge: c.MaxAge, MaxBytes: c.MaxBytes, Replicas: c.Replicas, Duplicates: 2 * time.Minute}) + if err != nil { + return fmt.Errorf("ensure stream %q: %w", c.Name, err) + } + if c.SnapshotsBucket != "" { + _, err = j.js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: c.SnapshotsBucket, Description: "current complete internal DNS bootstrap snapshots", History: 1, Storage: jetstream.FileStorage, Replicas: c.Replicas}) + if err != nil { + return fmt.Errorf("ensure snapshot bucket %q: %w", c.SnapshotsBucket, err) + } + } + return nil +} + +type ConsumerConfig struct { + Stream string `json:"stream"` + Durable string `json:"durable"` + FilterSubjects []string `json:"filterSubjects"` + AckWait time.Duration `json:"ackWait,omitempty"` + MaxAckPending int `json:"maxAckPending,omitempty"` +} + +type Message interface { + Subject() string + Envelope() model.Envelope + StreamSequence() uint64 + Ack(context.Context) error + Retry(time.Duration) error + Reject(string) error +} + +type Consumer struct{ c jetstream.Consumer } + +func (j *JetStream) Consumer(ctx context.Context, c ConsumerConfig) (*Consumer, error) { + if c.Stream == "" || !validDurable(c.Durable) || len(c.FilterSubjects) == 0 { + return nil, errors.New("stream, safe durable name, and filters are required") + } + cons, err := j.js.CreateOrUpdateConsumer(ctx, c.Stream, jetstream.ConsumerConfig{Durable: c.Durable, AckPolicy: jetstream.AckExplicitPolicy, DeliverPolicy: jetstream.DeliverAllPolicy, ReplayPolicy: jetstream.ReplayInstantPolicy, FilterSubjects: c.FilterSubjects, AckWait: defaultDuration(c.AckWait, 30*time.Second), MaxAckPending: defaultInt(c.MaxAckPending, 64)}) + if err != nil { + return nil, fmt.Errorf("ensure durable consumer %q: %w", c.Durable, err) + } + return &Consumer{c: cons}, nil +} + +// Next returns one event. Malformed envelopes are terminated so a poison event +// cannot block every later revision on an ordered durable. +func (c *Consumer) Next(ctx context.Context) (Message, error) { + m, err := c.c.Next(jetstream.FetchContext(ctx)) + if err != nil { + return nil, err + } + var env model.Envelope + if err := json.Unmarshal(m.Data(), &env); err != nil { + _ = m.TermWithReason("invalid JSON envelope") + return nil, fmt.Errorf("decode envelope: %w", err) + } + if err := env.Validate(); err != nil { + _ = m.TermWithReason("invalid DNS envelope") + return nil, err + } + meta, err := m.Metadata() + if err != nil { + _ = m.Nak() + return nil, err + } + return &message{msg: m, subject: m.Subject(), env: env, seq: meta.Sequence.Stream}, nil +} + +type message struct { + msg jetstream.Msg + subject string + env model.Envelope + seq uint64 +} + +func (m *message) Subject() string { return m.subject } +func (m *message) Envelope() model.Envelope { return m.env } +func (m *message) StreamSequence() uint64 { return m.seq } +func (m *message) Ack(ctx context.Context) error { return m.msg.DoubleAck(ctx) } +func (m *message) Retry(d time.Duration) error { return m.msg.NakWithDelay(d) } +func (m *message) Reject(reason string) error { return m.msg.TermWithReason(reason) } + +type SnapshotStore struct{ kv jetstream.KeyValue } + +func (j *JetStream) SnapshotStore(ctx context.Context, bucket string) (*SnapshotStore, error) { + if bucket == "" { + return nil, errors.New("snapshot bucket is required") + } + kv, err := j.js.KeyValue(ctx, bucket) + if err != nil { + return nil, fmt.Errorf("open snapshot bucket %q: %w", bucket, err) + } + return &SnapshotStore{kv: kv}, nil +} + +// Put stores a complete, already validated bootstrap envelope. It uses a KV +// compare-and-swap loop so a delayed publisher cannot replace a current +// bootstrap pointer with an older epoch or revision. +func (s *SnapshotStore) Put(ctx context.Context, key string, env model.Envelope) (uint64, error) { + if err := env.Validate(); err != nil { + return 0, err + } + if !validSnapshotKey(key) { + return 0, fmt.Errorf("invalid snapshot key %q", key) + } + b, err := json.Marshal(env) + if err != nil { + return 0, err + } + for attempt := 0; attempt < 8; attempt++ { + current, err := s.kv.Get(ctx, key) + if errors.Is(err, jetstream.ErrKeyNotFound) { + revision, err := s.kv.Create(ctx, key, b) + if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) { + continue + } + return revision, err + } + if err != nil { + return 0, err + } + var old model.Envelope + if err := json.Unmarshal(current.Value(), &old); err != nil { + return 0, fmt.Errorf("decode current snapshot %q: %w", key, err) + } + cmp := model.Compare(env.Epoch, env.Revision, old.Epoch, old.Revision) + if cmp < 0 { + return current.Revision(), nil + } + if cmp == 0 { + if model.Hash(current.Value()) != model.Hash(b) { + return 0, fmt.Errorf("snapshot %q has conflicting content at epoch %d revision %d", key, env.Epoch, env.Revision) + } + return current.Revision(), nil + } + revision, err := s.kv.Update(ctx, key, b, current.Revision()) + if errors.Is(err, jetstream.ErrKeyRevisionMismatch) { + continue + } + return revision, err + } + return 0, fmt.Errorf("snapshot %q CAS did not converge", key) +} + +func (s *SnapshotStore) Get(ctx context.Context, key string) (model.Envelope, error) { + entry, err := s.kv.Get(ctx, key) + if err != nil { + return model.Envelope{}, err + } + var env model.Envelope + if err := json.Unmarshal(entry.Value(), &env); err != nil { + return model.Envelope{}, err + } + return env, env.Validate() +} + +func (s *SnapshotStore) List(ctx context.Context, prefix string) ([]model.Envelope, error) { + keys, err := s.kv.Keys(ctx) + if err != nil { + return nil, err + } + var out []model.Envelope + for _, key := range keys { + if !strings.HasPrefix(key, prefix) { + continue + } + env, err := s.Get(ctx, key) + if err != nil { + return nil, fmt.Errorf("read snapshot %q: %w", key, err) + } + out = append(out, env) + } + return out, nil +} + +func validDurable(s string) bool { + if s == "" { + return false + } + return !strings.ContainsAny(s, " .*\\/>\t\r\n") +} +func validSnapshotKey(s string) bool { return s != "" && !strings.ContainsAny(s, " *>\t\r\n/") } +func defaultDuration(v, d time.Duration) time.Duration { + if v <= 0 { + return d + } + return v +} +func defaultInt(v, d int) int { + if v <= 0 { + return d + } + return v +} diff --git a/internal/internaldns/transport/jetstream_test.go b/internal/internaldns/transport/jetstream_test.go new file mode 100644 index 00000000..8a9e1456 --- /dev/null +++ b/internal/internaldns/transport/jetstream_test.go @@ -0,0 +1,55 @@ +package transport + +import "testing" + +func TestProductionTransportRequiresTLSAndOneAuthMethod(t *testing.T) { + t.Parallel() + base := Config{URL: "nats://example:4222", Name: "agent"} + if err := base.validate(); err == nil { + t.Fatal("plaintext production NATS accepted") + } + base.TLS = &TLSConfig{ServerName: "nats.example"} + if err := base.validate(); err == nil { + t.Fatal("unauthenticated production NATS accepted") + } + base.Token = "secret" + if err := base.validate(); err != nil { + t.Fatal(err) + } + base.Username, base.Password = "also", "configured" + if err := base.validate(); err == nil { + t.Fatal("multiple authentication methods accepted") + } +} + +func TestDevelopmentTransportMustBeExplicit(t *testing.T) { + t.Parallel() + if err := (Config{URL: "nats://127.0.0.1:4222", Name: "e2e", AllowInsecure: true}).validate(); err != nil { + t.Fatal(err) + } +} + +func TestDurableNameRejectsQueueSharingSyntax(t *testing.T) { + t.Parallel() + for _, bad := range []string{"", "replica.one", "replica one", "replica*"} { + if validDurable(bad) { + t.Fatalf("unsafe durable %q accepted", bad) + } + } + if !validDurable("auth-replica-one") { + t.Fatal("safe durable rejected") + } +} + +func TestBrokerMessageIDIsNamespacedBySubject(t *testing.T) { + t.Parallel() + eventID := "event-123" + publication := brokerMessageID("internaldns.pub.us-east.s1.manifest.zone-a", eventID) + ack := brokerMessageID("internaldns.ack.us-east.s1.member-a", eventID) + if publication == ack { + t.Fatal("the same event ID collided across independently authorized subjects") + } + if publication != brokerMessageID("internaldns.pub.us-east.s1.manifest.zone-a", eventID) { + t.Fatal("message ID is not stable for an ambiguous retry") + } +} From ddb235eaa18f8d95f5c02f2b163f7e8ddd469012 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:07:40 -0500 Subject: [PATCH 10/28] Validate publication delivery contracts with schemas and CEL --- api/v1alpha1/internaldns_service_types.go | 80 +++++++++----- ...rnaldns_service_validation_envtest_test.go | 100 ++++++++++++++++++ .../internaldns_validation_envtest_test.go | 2 + ...ing.miloapis.com_dnspublicationchunks.yaml | 5 + ....miloapis.com_dnspublicationmanifests.yaml | 20 ++++ ...miloapis.com_dnspublicationownerships.yaml | 16 +++ ...king.miloapis.com_dnsresolverbindings.yaml | 16 +++ ...ing.miloapis.com_dnstransportoutboxes.yaml | 6 ++ 8 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 api/v1alpha1/internaldns_service_validation_envtest_test.go diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go index b55071d0..e97eb874 100644 --- a/api/v1alpha1/internaldns_service_types.go +++ b/api/v1alpha1/internaldns_service_types.go @@ -25,6 +25,8 @@ type DNSResolverBindingSpec struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding source identity is immutable" // +kubebuilder:validation:XValidation:rule="self.projectUID.size() > 0 && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 && self.accessBindingRef.name.size() > 0 && has(self.accessBindingRef.uid) && self.accessBindingRef.uid.size() > 0",message="binding source requires project UID and UID-pinned context and access references" type DNSResolverBindingSource struct { + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MaxLength=128 ProjectUID types.UID `json:"projectUID"` ResolverContextRef DNSObjectReference `json:"resolverContextRef"` AccessBindingRef DNSObjectReference `json:"accessBindingRef"` @@ -32,8 +34,12 @@ type DNSResolverBindingSource struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="binding placement is immutable" type DNSResolverBindingPlacement struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Region string `json:"region"` - Shard string `json:"shard"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Shard string `json:"shard"` } // +kubebuilder:validation:XValidation:rule="self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() > 0)",message="zone references must pin name and UID" @@ -55,6 +61,7 @@ type DNSResolverBindingListeners struct { type DNSResolverBindingListener struct { // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Address string `json:"address"` // +kubebuilder:validation:Enum=53 Port int32 `json:"port"` @@ -103,26 +110,36 @@ type DNSResolverBindingList struct { } type DNSPublicationChunkReference struct { - Name string `json:"name"` + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` SHA256 string `json:"sha256"` - Size int32 `json:"size"` + // +kubebuilder:validation:Minimum=1 + Size int32 `json:"size"` } // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication manifests are immutable" +// +kubebuilder:validation:XValidation:rule="self.zoneRef.name.size() > 0 && has(self.zoneRef.uid) && self.zoneRef.uid.size() > 0",message="zoneRef requires name and UID" +// +kubebuilder:validation:XValidation:rule="!has(self.previousRevision) || (self.previousRevision >= 0 && self.previousRevision < self.revision)",message="previousRevision must precede revision" +// +kubebuilder:validation:XValidation:rule="(has(self.tombstone) && self.tombstone) ? (self.chunks.size() == 0 && self.contentHash == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') : self.chunks.size() > 0",message="withdrawal requires empty content; publication requires chunks" type DNSPublicationManifestSpec struct { - ZoneRef DNSObjectReference `json:"zoneRef"` - ZoneApex string `json:"zoneApex"` - ContextUIDs []types.UID `json:"contextUIDs"` - WriterEpoch int64 `json:"writerEpoch"` - Revision int64 `json:"revision"` - PreviousRevision int64 `json:"previousRevision,omitempty"` - Tombstone bool `json:"tombstone,omitempty"` - Chunks []DNSPublicationChunkReference `json:"chunks"` - ContentHash string `json:"contentHash"` - GeneratedAt metav1.Time `json:"generatedAt"` - ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` - RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` - ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` + ZoneRef DNSObjectReference `json:"zoneRef"` + // +kubebuilder:validation:MinLength=1 + ZoneApex string `json:"zoneApex"` + ContextUIDs []types.UID `json:"contextUIDs"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + PreviousRevision int64 `json:"previousRevision,omitempty"` + Tombstone bool `json:"tombstone,omitempty"` + Chunks []DNSPublicationChunkReference `json:"chunks"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` + ContentHash string `json:"contentHash"` + GeneratedAt metav1.Time `json:"generatedAt"` + ContributionFences []DNSContributionFence `json:"contributionFences,omitempty"` + RegistrationFences []DNSRegistrationFence `json:"registrationFences,omitempty"` + ServingTargets []DNSPublicationTarget `json:"servingTargets,omitempty"` } type DNSPublicationTarget struct { @@ -173,11 +190,16 @@ type DNSPublicationManifestList struct { // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="publication chunks are immutable" type DNSPublicationChunkSpec struct { ManifestUID types.UID `json:"manifestUID,omitempty"` - WriterEpoch int64 `json:"writerEpoch"` - Revision int64 `json:"revision"` - Index int32 `json:"index"` - SHA256 string `json:"sha256"` - Payload []byte `json:"payload"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 + Revision int64 `json:"revision"` + // +kubebuilder:validation:Minimum=0 + Index int32 `json:"index"` + // +kubebuilder:validation:Pattern=`^[a-fA-F0-9]{64}$` + SHA256 string `json:"sha256"` + // +kubebuilder:validation:MinLength=1 + Payload []byte `json:"payload"` } // +kubebuilder:object:root=true @@ -235,10 +257,20 @@ type DNSTransportOutboxList struct { // DNSPublicationOwnership is a durable compare-and-swap record for a zone // compiler. A lease takeover increments WriterEpoch and fences delayed holders. +// +kubebuilder:validation:XValidation:rule="self.writerEpoch >= oldSelf.writerEpoch && self.nextRevision >= oldSelf.nextRevision",message="publication ownership fences must not decrease" +// +kubebuilder:validation:XValidation:rule="self.holderIdentity == oldSelf.holderIdentity || self.writerEpoch > oldSelf.writerEpoch",message="ownership transfer requires a new writer epoch" type DNSPublicationOwnershipSpec struct { - ZoneUID types.UID `json:"zoneUID"` - HolderIdentity string `json:"holderIdentity"` - WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="zoneUID is immutable" + ZoneUID types.UID `json:"zoneUID"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=512 + HolderIdentity string `json:"holderIdentity"` + // +kubebuilder:validation:Minimum=1 + WriterEpoch int64 `json:"writerEpoch"` + // +kubebuilder:validation:Minimum=1 NextRevision int64 `json:"nextRevision"` LeaseUntil metav1.Time `json:"leaseUntil"` LastContentHash string `json:"lastContentHash,omitempty"` diff --git a/api/v1alpha1/internaldns_service_validation_envtest_test.go b/api/v1alpha1/internaldns_service_validation_envtest_test.go new file mode 100644 index 00000000..3c1c1efd --- /dev/null +++ b/api/v1alpha1/internaldns_service_validation_envtest_test.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "k8s.io/apimachinery/pkg/types" + "strings" + "testing" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func validateServiceDNSContracts(t *testing.T, cl client.Client) { + t.Helper() + ctx := context.Background() + invalid := func(t *testing.T, err error) { + t.Helper() + if !apierrors.IsInvalid(err) { + t.Fatalf("expected API validation rejection, got %v", err) + } + } + manifest := func(name string) *DNSPublicationManifest { + return &DNSPublicationManifest{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSPublicationManifestSpec{ZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, ZoneApex: "datum.internal", ContextUIDs: []types.UID{}, WriterEpoch: 1, Revision: 2, PreviousRevision: 1, Chunks: []DNSPublicationChunkReference{{Name: "chunk", SHA256: strings.Repeat("a", 64), Size: 8}}, ContentHash: strings.Repeat("b", 64), GeneratedAt: metav1.Now()}} + } + for _, tc := range []struct { + name string + mutate func(*DNSPublicationManifest) + }{ + {"manifest-zero-epoch", func(a *DNSPublicationManifest) { a.Spec.WriterEpoch = 0 }}, + {"manifest-zero-revision", func(a *DNSPublicationManifest) { a.Spec.Revision = 0 }}, + {"manifest-future-predecessor", func(a *DNSPublicationManifest) { a.Spec.PreviousRevision = a.Spec.Revision }}, + {"manifest-invalid-hash", func(a *DNSPublicationManifest) { a.Spec.ContentHash = "invalid" }}, + {"manifest-empty-publication", func(a *DNSPublicationManifest) { a.Spec.Chunks = []DNSPublicationChunkReference{} }}, + {"manifest-withdrawal-content", func(a *DNSPublicationManifest) { a.Spec.Tombstone = true }}, + {"manifest-empty-chunk", func(a *DNSPublicationManifest) { a.Spec.Chunks[0].Size = 0 }}, + } { + t.Run(tc.name, func(t *testing.T) { a := manifest(tc.name); tc.mutate(a); invalid(t, cl.Create(ctx, a)) }) + } + t.Run("manifest-status-and-immutability", func(t *testing.T) { + a := manifest("manifest") + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Status.ExportState = "Delivered" + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.Revision++ + invalid(t, cl.Update(ctx, a)) + }) + t.Run("manifest-withdrawal", func(t *testing.T) { + a := manifest("withdrawal") + a.Spec.Tombstone = true + a.Spec.Chunks = []DNSPublicationChunkReference{} + a.Spec.ContentHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + }) + t.Run("chunk-empty-payload", func(t *testing.T) { + a := &DNSPublicationChunk{ObjectMeta: metav1.ObjectMeta{Name: "empty-chunk", Namespace: "validation"}, Spec: DNSPublicationChunkSpec{WriterEpoch: 1, Revision: 1, Index: 0, SHA256: strings.Repeat("a", 64), Payload: []byte{}}} + invalid(t, cl.Create(ctx, a)) + }) + owner := func(name string) *DNSPublicationOwnership { + return &DNSPublicationOwnership{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSPublicationOwnershipSpec{ZoneUID: "zone-uid", HolderIdentity: "worker-a", WriterEpoch: 2, NextRevision: 3, LeaseUntil: metav1.NewTime(time.Now().Add(time.Minute))}} + } + for _, tc := range []struct { + name string + mutate func(*DNSPublicationOwnership) + }{ + {"owner-change-zone", func(a *DNSPublicationOwnership) { a.Spec.ZoneUID = "other" }}, + {"owner-epoch-rollback", func(a *DNSPublicationOwnership) { a.Spec.WriterEpoch-- }}, + {"owner-revision-rollback", func(a *DNSPublicationOwnership) { a.Spec.NextRevision-- }}, + {"owner-unfenced-transfer", func(a *DNSPublicationOwnership) { a.Spec.HolderIdentity = "worker-b" }}, + } { + t.Run(tc.name, func(t *testing.T) { + a := owner(tc.name) + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + tc.mutate(a) + invalid(t, cl.Update(ctx, a)) + }) + } + t.Run("owner-fenced-transfer", func(t *testing.T) { + a := owner("transfer") + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.HolderIdentity = "worker-b" + a.Spec.WriterEpoch++ + if err := cl.Update(ctx, a); err != nil { + t.Fatal(err) + } + }) +} diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index a4f9578e..4426872b 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -143,4 +143,6 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { a.Spec.GrantRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) + validateServiceDNSContracts(t, cl) + } diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml index 72f5fe48..ef9d0efb 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationchunks.yaml @@ -39,6 +39,7 @@ spec: properties: index: format: int32 + minimum: 0 type: integer manifestUID: description: |- @@ -48,14 +49,18 @@ spec: type: string payload: format: byte + minLength: 1 type: string revision: format: int64 + minimum: 1 type: integer sha256: + pattern: ^[a-fA-F0-9]{64}$ type: string writerEpoch: format: int64 + minimum: 1 type: integer required: - index diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml index 80dc8429..1a8845d5 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationmanifests.yaml @@ -41,11 +41,14 @@ spec: items: properties: name: + minLength: 1 type: string sha256: + pattern: ^[a-fA-F0-9]{64}$ type: string size: format: int32 + minimum: 1 type: integer required: - name @@ -54,6 +57,7 @@ spec: type: object type: array contentHash: + pattern: ^[a-fA-F0-9]{64}$ type: string contextUIDs: items: @@ -122,6 +126,7 @@ spec: type: array revision: format: int64 + minimum: 1 type: integer servingTargets: items: @@ -139,8 +144,10 @@ spec: type: boolean writerEpoch: format: int64 + minimum: 1 type: integer zoneApex: + minLength: 1 type: string zoneRef: description: |- @@ -149,14 +156,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -174,6 +184,16 @@ spec: x-kubernetes-validations: - message: publication manifests are immutable rule: oldSelf == self + - message: zoneRef requires name and UID + rule: self.zoneRef.name.size() > 0 && has(self.zoneRef.uid) && self.zoneRef.uid.size() + > 0 + - message: previousRevision must precede revision + rule: '!has(self.previousRevision) || (self.previousRevision >= 0 && + self.previousRevision < self.revision)' + - message: withdrawal requires empty content; publication requires chunks + rule: '(has(self.tombstone) && self.tombstone) ? (self.chunks.size() + == 0 && self.contentHash == ''e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'') + : self.chunks.size() > 0' status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml index 2e9ae926..5f53fe6b 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnspublicationownerships.yaml @@ -43,6 +43,8 @@ spec: activeManifestName: type: string holderIdentity: + maxLength: 512 + minLength: 1 type: string lastContentHash: type: string @@ -51,16 +53,23 @@ spec: type: string nextRevision: format: int64 + minimum: 1 type: integer writerEpoch: format: int64 + minimum: 1 type: integer zoneUID: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 + minLength: 1 type: string + x-kubernetes-validations: + - message: zoneUID is immutable + rule: self == oldSelf required: - holderIdentity - leaseUntil @@ -68,6 +77,13 @@ spec: - writerEpoch - zoneUID type: object + x-kubernetes-validations: + - message: publication ownership fences must not decrease + rule: self.writerEpoch >= oldSelf.writerEpoch && self.nextRevision >= + oldSelf.nextRevision + - message: ownership transfer requires a new writer epoch + rule: self.holderIdentity == oldSelf.holderIdentity || self.writerEpoch + > oldSelf.writerEpoch required: - spec type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml index adab7d82..4875ae95 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -68,6 +68,7 @@ spec: node: properties: address: + maxLength: 253 minLength: 1 type: string port: @@ -93,6 +94,7 @@ spec: regional: properties: address: + maxLength: 253 minLength: 1 type: string port: @@ -134,14 +136,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -161,8 +166,12 @@ spec: placement: properties: region: + maxLength: 253 + minLength: 1 type: string shard: + maxLength: 253 + minLength: 1 type: string required: - region @@ -180,14 +189,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -197,6 +209,7 @@ spec: UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string resolverContextRef: description: |- @@ -205,14 +218,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name diff --git a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml index dab793c6..cd4e5287 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnstransportoutboxes.yaml @@ -50,14 +50,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name @@ -69,14 +72,17 @@ spec: properties: generation: format: int64 + minimum: 0 type: integer name: + maxLength: 253 type: string uid: description: |- UID is a type that holds unique ID values, including UUIDs. Because we don't ONLY use UUIDs, this is an alias to string. Being a type captures intent and helps make sure that UIDs and names do not get conflated. + maxLength: 128 type: string required: - name From 69a8440981d13acd4adf5f85382de5c9b33c576c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 17:14:32 -0500 Subject: [PATCH 11/28] Validate inactive contribution epoch binding without publisher observations --- internal/internaldns/admission/handler.go | 14 +++++++++++++- internal/internaldns/admission/handler_test.go | 10 ++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 57f9c533..f625740c 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -140,6 +140,7 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID return err } } + bindEpoch := false if req.SubResource == "status" { previous, current := old.Status, c.Status previous.Conditions, current.Conditions = nil, nil @@ -147,6 +148,11 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if reflect.DeepEqual(previous, current) { return nil } + // Binding an issued epoch cannot introduce or renew endpoint eligibility. + if previous.WriterEpoch == 0 && previous.ObservedGeneration == 0 && previous.Sequence == 0 && !previous.Eligible && previous.ValidUntil == nil && current.WriterEpoch > 0 { + current.WriterEpoch = 0 + bindEpoch = reflect.DeepEqual(previous, current) + } } var g dnsv1alpha1.DNSContributionGrant if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.GrantRef.Name}, &g); err != nil { @@ -155,7 +161,7 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if g.UID != c.Spec.GrantRef.UID || !g.DeletionTimestamp.IsZero() { return fmt.Errorf("grant lifetime is not current") } - if clusterUID == "" || g.Spec.Principal.ClusterUID != clusterUID || g.Spec.Principal.Subject != req.UserInfo.Username { + if clusterUID == "" || g.Spec.Principal.ClusterUID != clusterUID || (!bindEpoch && g.Spec.Principal.Subject != req.UserInfo.Username) { return fmt.Errorf("caller does not match the grant's authenticated producer principal") } if g.Spec.RegistrationRef.UID != c.Spec.RegistrationRef.UID || g.Spec.RegistrationRef.Name != c.Spec.RegistrationRef.Name { @@ -188,6 +194,12 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID return fmt.Errorf("record type %s is outside the writer's scope", rrset.RecordType) } } + if bindEpoch { + if c.Status.WriterEpoch != g.Status.ActiveWriterEpoch { + return fmt.Errorf("writer epoch is retired or not issued") + } + return nil + } if req.SubResource == "status" { if !reflect.DeepEqual(old.Status.Conditions, c.Status.Conditions) || old.Status.PublishedRevision != c.Status.PublishedRevision { return fmt.Errorf("publisher observations cannot change publication results") diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index d1fb1c31..02996320 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -56,6 +56,12 @@ func TestContributionAuthenticatedWriter(t *testing.T) { }{ {"scoped fresh producer", producer, nil, true}, {"another producer", "system:serviceaccount:connector-system:dns-publisher", nil, false}, + {"controller binds issued epoch", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { + c.Status.Conditions = []metav1.Condition{{Type: "AuthorityBound", Status: metav1.ConditionTrue}} + }, true}, + {"controller cannot bind another epoch", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 99 }, false}, + {"epoch binding cannot assert health", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Eligible = true }, false}, + {"retired epoch", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 7 }, false}, {"unchanged observation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 17 }, true}, {"decreasing sequence", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 16 }, false}, @@ -71,6 +77,10 @@ func TestContributionAuthenticatedWriter(t *testing.T) { c := contribution.DeepCopy() old := contribution.DeepCopy() old.Status.Sequence = 17 + if tc.name == "controller binds issued epoch" || tc.name == "controller cannot bind another epoch" || tc.name == "epoch binding cannot assert health" { + old.Status = dnsv1alpha1.DNSRecordContributionStatus{} + c.Status = dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 8} + } if tc.mutate != nil { tc.mutate(c) } From bc7a505804c4d1d1560f3e01bd9b074da06316f2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:15:58 -0500 Subject: [PATCH 12/28] Format publication schema validation tests --- api/v1alpha1/internaldns_service_validation_envtest_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/internaldns_service_validation_envtest_test.go b/api/v1alpha1/internaldns_service_validation_envtest_test.go index 3c1c1efd..7a356145 100644 --- a/api/v1alpha1/internaldns_service_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_service_validation_envtest_test.go @@ -4,11 +4,12 @@ package v1alpha1 import ( "context" - "k8s.io/apimachinery/pkg/types" "strings" "testing" "time" + "k8s.io/apimachinery/pkg/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" From 695a45d15b9cc479025387cc67aa007d8de7af4f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 09:49:30 -0500 Subject: [PATCH 13/28] fix: Keep DNS APIs context scoped --- ...ternaldns_context_boundary_envtest_test.go | 67 +++++++ api/v1alpha1/internaldns_project_types.go | 91 +++++---- api/v1alpha1/internaldns_types_test.go | 11 +- .../internaldns_validation_envtest_test.go | 1 + api/v1alpha1/zz_generated.deepcopy.go | 73 +++++++- ...ing.miloapis.com_dnsmanagednamespaces.yaml | 10 +- ...orking.miloapis.com_dnsnamingpolicies.yaml | 175 ++++++++++-------- ...king.miloapis.com_dnszoneassociations.yaml | 59 +----- internal/internaldns/admission/handler.go | 65 +++---- 9 files changed, 322 insertions(+), 230 deletions(-) create mode 100644 api/v1alpha1/internaldns_context_boundary_envtest_test.go diff --git a/api/v1alpha1/internaldns_context_boundary_envtest_test.go b/api/v1alpha1/internaldns_context_boundary_envtest_test.go new file mode 100644 index 00000000..2c099c0c --- /dev/null +++ b/api/v1alpha1/internaldns_context_boundary_envtest_test.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func validateResolverContextBoundaries(t *testing.T, cl client.Client) { + t.Helper() + for _, kind := range []string{"DNSZoneAssociation", "DNSNamingPolicy"} { + t.Run(kind+"-context-boundary", func(t *testing.T) { + newObject := func(name string) *unstructured.Unstructured { + return &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": GroupVersion.String(), "kind": kind, + "metadata": map[string]any{"name": name, "namespace": "validation"}, + "spec": map[string]any{ + "dnsZoneRef": map[string]any{"name": "zone", "uid": "zone-uid"}, + "additionalNames": []any{}, + }, + }} + } + ctx := context.Background() + for _, ref := range []map[string]any{nil, {"name": ""}, {"name": "", "uid": "context-uid"}} { + o := newObject("context-required") + if ref != nil { + if err := unstructured.SetNestedMap(o.Object, ref, "spec", "resolverContextRef"); err != nil { + t.Fatal(err) + } + } + if err := unstructured.SetNestedMap(o.Object, map[string]any{"name": "network", "uid": "network-uid"}, "spec", "vpcRef"); err != nil { + t.Fatal(err) + } + if err := cl.Create(ctx, o); !apierrors.IsInvalid(err) { + t.Fatalf("expected rejection without a named DNS context, got %v", err) + } + } + o := newObject("context-selected") + if err := unstructured.SetNestedMap(o.Object, map[string]any{"name": "context", "uid": "context-uid"}, "spec", "resolverContextRef"); err != nil { + t.Fatal(err) + } + if err := unstructured.SetNestedMap(o.Object, map[string]any{"name": "network", "uid": "network-uid"}, "spec", "vpcRef"); err != nil { + t.Fatal(err) + } + if err := cl.Create(ctx, o); err != nil { + t.Fatal(err) + } + if err := cl.Get(ctx, client.ObjectKeyFromObject(o), o); err != nil { + t.Fatal(err) + } + if _, found, err := unstructured.NestedMap(o.Object, "spec", "vpcRef"); err != nil || found { + t.Fatalf("network reference was retained in the DNS API: found=%t err=%v", found, err) + } + if err := unstructured.SetNestedMap(o.Object, map[string]any{"name": "", "uid": "context-uid"}, "spec", "resolverContextRef"); err != nil { + t.Fatal(err) + } + if err := cl.Update(ctx, o); !apierrors.IsInvalid(err) { + t.Fatalf("expected rejection when clearing the DNS context, got %v", err) + } + }) + } +} diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 04bbd19c..ee23cc8d 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -22,20 +22,17 @@ type DNSObjectReference struct { Generation int64 `json:"generation,omitempty"` } -// +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" -// +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" +// +kubebuilder:validation:XValidation:rule="self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0",message="resolverContextRef is required" type DNSZoneAssociationSpec struct { DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - VPCRef DNSObjectReference `json:"vpcRef,omitempty"` - ResolverContextRef DNSObjectReference `json:"resolverContextRef,omitempty"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` Managed bool `json:"managed,omitempty"` } type DNSZoneAssociationStatus struct { ResolvedDNSZoneRef DNSObjectReference `json:"resolvedDNSZoneRef,omitempty"` - ResolvedVPCRef DNSObjectReference `json:"resolvedVPCRef,omitempty"` ResolvedResolverContextRef DNSObjectReference `json:"resolvedResolverContextRef,omitempty"` BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -316,40 +313,60 @@ type DNSRecordContributionList struct { Items []DNSRecordContribution `json:"items"` } -// +kubebuilder:validation:XValidation:rule="(has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0)",message="exactly one UID-pinned vpcRef or resolverContextRef is required" -// +kubebuilder:validation:XValidation:rule="!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0))",message="a nonempty vpcRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) || self.resolverContextRef.generation == 0)) || (self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0))",message="a nonempty resolverContextRef must pin name and UID" // +kubebuilder:validation:XValidation:rule="!has(self.dnsZoneRef) || self.dnsZoneRef.name.size() == 0 || (has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0)",message="a selected dnsZoneRef must pin the zone UID" +// +kubebuilder:validation:XValidation:rule="self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0",message="resolverContextRef is required" type DNSNamingPolicySpec struct { - VPCRef DNSObjectReference `json:"vpcRef,omitempty"` - ResolverContextRef DNSObjectReference `json:"resolverContextRef,omitempty"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` - // Legacy single-rule fields are retained during the v1alpha1 transition. - DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` - Product string `json:"product,omitempty"` - ScopePrefix string `json:"scopePrefix,omitempty"` - Priority int32 `json:"priority,omitempty"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + ScopePrefix string `json:"scopePrefix,omitempty"` + Priority int32 `json:"priority,omitempty"` } -// +kubebuilder:validation:Enum=InstanceIdentity;ServiceDiscovery;ServiceVIP;ServiceExport -type DNSRegistrationClass string - -const ( - DNSRegistrationClassInstanceIdentity DNSRegistrationClass = "InstanceIdentity" - DNSRegistrationClassServiceDiscovery DNSRegistrationClass = "ServiceDiscovery" - DNSRegistrationClassServiceVIP DNSRegistrationClass = "ServiceVIP" - DNSRegistrationClassServiceExport DNSRegistrationClass = "ServiceExport" -) - // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSAdditionalNameRule struct { - RegistrationClass DNSRegistrationClass `json:"registrationClass"` - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - NamePrefix string `json:"namePrefix"` + Selector *DNSRegistrationSelector `json:"selector,omitempty"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + NamePrefix string `json:"namePrefix"` +} + +// DNSRegistrationSelector matches registration labels using Kubernetes selector semantics. +type DNSRegistrationSelector struct { + // +kubebuilder:validation:MaxProperties=64 + MatchLabels map[string]string `json:"matchLabels,omitempty"` + // +kubebuilder:validation:MaxItems=64 + MatchExpressions []DNSRegistrationSelectorRequirement `json:"matchExpressions,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="(self.operator in ['In', 'NotIn']) ? (has(self.values) && self.values.size() > 0) : (!has(self.values) || self.values.size() == 0)",message="selector operator requires compatible values" +type DNSRegistrationSelectorRequirement struct { + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=317 + Key string `json:"key"` + // +kubebuilder:validation:Enum=In;NotIn;Exists;DoesNotExist + Operator metav1.LabelSelectorOperator `json:"operator"` + // +kubebuilder:validation:MaxItems=64 + // +kubebuilder:validation:items:MaxLength=63 + // +kubebuilder:validation:items:Pattern=`^([A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?)?$` + Values []string `json:"values,omitempty"` +} + +// AsLabelSelector converts the API selector for Kubernetes label matching. +func (s *DNSRegistrationSelector) AsLabelSelector() *metav1.LabelSelector { + if s == nil { + return nil + } + out := &metav1.LabelSelector{MatchLabels: s.MatchLabels} + for _, requirement := range s.MatchExpressions { + out.MatchExpressions = append(out.MatchExpressions, metav1.LabelSelectorRequirement{ + Key: requirement.Key, Operator: requirement.Operator, Values: requirement.Values, + }) + } + return out } type DNSNamingPolicyStatus struct { - ResolvedVPCRef DNSObjectReference `json:"resolvedVPCRef,omitempty"` ResolvedResolverContextRef DNSObjectReference `json:"resolvedResolverContextRef,omitempty"` ResolvedDNSZoneRef DNSObjectReference `json:"resolvedDNSZoneRef,omitempty"` ResolvedAdditionalNames []DNSResolvedAdditionalNameRule `json:"resolvedAdditionalNames,omitempty"` @@ -357,9 +374,9 @@ type DNSNamingPolicyStatus struct { } type DNSResolvedAdditionalNameRule struct { - RegistrationClass DNSRegistrationClass `json:"registrationClass"` - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - NamePrefix string `json:"namePrefix"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + NamePrefix string `json:"namePrefix"` } // +kubebuilder:object:root=true @@ -378,14 +395,12 @@ type DNSNamingPolicyList struct { Items []DNSNamingPolicy `json:"items"` } -// DNSManagedNamespace is the DNS-owned lookup and lifecycle record for the -// canonical private namespace of a VPC. Product controllers read its status; -// only the DNS platform controller writes the object. +// DNSManagedNamespace tracks a resolver context's canonical private namespace. type DNSManagedNamespaceSpec struct { - ProjectUID types.UID `json:"projectUID"` - VPCRef DNSObjectReference `json:"vpcRef"` - DomainSuffix string `json:"domainSuffix"` - DNSZoneClassName string `json:"dnsZoneClassName"` + ProjectUID types.UID `json:"projectUID"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` + DomainSuffix string `json:"domainSuffix"` + DNSZoneClassName string `json:"dnsZoneClassName"` } type DNSManagedNamespaceStatus struct { diff --git a/api/v1alpha1/internaldns_types_test.go b/api/v1alpha1/internaldns_types_test.go index 0929a7fc..14fe6d11 100644 --- a/api/v1alpha1/internaldns_types_test.go +++ b/api/v1alpha1/internaldns_types_test.go @@ -5,13 +5,10 @@ import ( "testing" ) -// Value-typed references with omitempty encode their unselected peer as {}. -// The CRD CEL deliberately tests a complete name/UID pin, rather than mere -// property presence, so this normal Go client representation remains valid. -func TestConsumerSelectorJSONUsesEmptyUnselectedReference(t *testing.T) { +func TestDNSPoliciesSelectResolverContexts(t *testing.T) { for name, value := range map[string]any{ "association context": DNSZoneAssociationSpec{ResolverContextRef: DNSObjectReference{Name: "ctx", UID: "ctx-uid"}}, - "naming legacy": DNSNamingPolicySpec{VPCRef: DNSObjectReference{Name: "vpc", UID: "vpc-uid"}}, + "naming context": DNSNamingPolicySpec{ResolverContextRef: DNSObjectReference{Name: "ctx", UID: "ctx-uid"}}, } { t.Run(name, func(t *testing.T) { data, err := json.Marshal(value) @@ -22,8 +19,8 @@ func TestConsumerSelectorJSONUsesEmptyUnselectedReference(t *testing.T) { if err := json.Unmarshal(data, &object); err != nil { t.Fatal(err) } - if _, hasVPC := object["vpcRef"]; !hasVPC { - t.Fatal("expected value-typed vpcRef property") + if _, hasVPC := object["vpcRef"]; hasVPC { + t.Fatal("DNS policies must not expose VPC references") } if _, hasContext := object["resolverContextRef"]; !hasContext { t.Fatal("expected value-typed resolverContextRef property") diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index a4f9578e..e9ae53d3 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -48,6 +48,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { if err := cl.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "validation"}}); err != nil { t.Fatal(err) } + validateResolverContextBoundaries(t, cl) requireInvalid := func(t *testing.T, err error) { t.Helper() if !apierrors.IsInvalid(err) { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9abcf217..8c13b75b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -90,6 +90,11 @@ func (in *CNAMERecordSpec) DeepCopy() *CNAMERecordSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSAdditionalNameRule) DeepCopyInto(out *DNSAdditionalNameRule) { *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(DNSRegistrationSelector) + (*in).DeepCopyInto(*out) + } out.DNSZoneRef = in.DNSZoneRef } @@ -295,7 +300,7 @@ func (in *DNSManagedNamespaceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSManagedNamespaceSpec) DeepCopyInto(out *DNSManagedNamespaceSpec) { *out = *in - out.VPCRef = in.VPCRef + out.ResolverContextRef = in.ResolverContextRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceSpec. @@ -394,12 +399,13 @@ func (in *DNSNamingPolicyList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSNamingPolicySpec) DeepCopyInto(out *DNSNamingPolicySpec) { *out = *in - out.VPCRef = in.VPCRef out.ResolverContextRef = in.ResolverContextRef if in.AdditionalNames != nil { in, out := &in.AdditionalNames, &out.AdditionalNames *out = make([]DNSAdditionalNameRule, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.DNSZoneRef = in.DNSZoneRef } @@ -417,13 +423,14 @@ func (in *DNSNamingPolicySpec) DeepCopy() *DNSNamingPolicySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSNamingPolicyStatus) DeepCopyInto(out *DNSNamingPolicyStatus) { *out = *in - out.ResolvedVPCRef = in.ResolvedVPCRef out.ResolvedResolverContextRef = in.ResolvedResolverContextRef out.ResolvedDNSZoneRef = in.ResolvedDNSZoneRef if in.ResolvedAdditionalNames != nil { in, out := &in.ResolvedAdditionalNames, &out.ResolvedAdditionalNames *out = make([]DNSResolvedAdditionalNameRule, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -753,6 +760,55 @@ func (in *DNSRegistrationList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistrationSelector) DeepCopyInto(out *DNSRegistrationSelector) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.MatchExpressions != nil { + in, out := &in.MatchExpressions, &out.MatchExpressions + *out = make([]DNSRegistrationSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationSelector. +func (in *DNSRegistrationSelector) DeepCopy() *DNSRegistrationSelector { + if in == nil { + return nil + } + out := new(DNSRegistrationSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRegistrationSelectorRequirement) DeepCopyInto(out *DNSRegistrationSelectorRequirement) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRegistrationSelectorRequirement. +func (in *DNSRegistrationSelectorRequirement) DeepCopy() *DNSRegistrationSelectorRequirement { + if in == nil { + return nil + } + out := new(DNSRegistrationSelectorRequirement) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRegistrationSpec) DeepCopyInto(out *DNSRegistrationSpec) { *out = *in @@ -804,6 +860,11 @@ func (in *DNSRegistrationStatus) DeepCopy() *DNSRegistrationStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSResolvedAdditionalNameRule) DeepCopyInto(out *DNSResolvedAdditionalNameRule) { *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } out.DNSZoneRef = in.DNSZoneRef } @@ -1196,7 +1257,6 @@ func (in *DNSZoneAssociationList) DeepCopyObject() runtime.Object { func (in *DNSZoneAssociationSpec) DeepCopyInto(out *DNSZoneAssociationSpec) { *out = *in out.DNSZoneRef = in.DNSZoneRef - out.VPCRef = in.VPCRef out.ResolverContextRef = in.ResolverContextRef } @@ -1214,7 +1274,6 @@ func (in *DNSZoneAssociationSpec) DeepCopy() *DNSZoneAssociationSpec { func (in *DNSZoneAssociationStatus) DeepCopyInto(out *DNSZoneAssociationStatus) { *out = *in out.ResolvedDNSZoneRef = in.ResolvedDNSZoneRef - out.ResolvedVPCRef = in.ResolvedVPCRef out.ResolvedResolverContextRef = in.ResolvedResolverContextRef if in.BindingRef != nil { in, out := &in.BindingRef, &out.BindingRef diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml index 5b4c9ecc..c57ef52a 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml @@ -36,10 +36,8 @@ spec: metadata: type: object spec: - description: |- - DNSManagedNamespace is the DNS-owned lookup and lifecycle record for the - canonical private namespace of a VPC. Product controllers read its status; - only the DNS platform controller writes the object. + description: DNSManagedNamespace tracks a resolver context's canonical + private namespace. properties: dnsZoneClassName: type: string @@ -51,7 +49,7 @@ spec: 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 - vpcRef: + resolverContextRef: description: |- DNSObjectReference pins a namespaced object by API-assigned UID. Generation is used where a writer must also be fenced against policy changes. @@ -77,7 +75,7 @@ spec: - dnsZoneClassName - domainSuffix - projectUID - - vpcRef + - resolverContextRef type: object status: properties: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml index c5df3cd2..e835d310 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -64,17 +64,53 @@ spec: type: object namePrefix: type: string - registrationClass: - enum: - - InstanceIdentity - - ServiceDiscovery - - ServiceVIP - - ServiceExport - type: string + selector: + description: DNSRegistrationSelector matches registration labels + using Kubernetes selector semantics. + properties: + matchExpressions: + items: + properties: + key: + maxLength: 317 + minLength: 1 + type: string + operator: + description: A label selector operator is the set + of operators that can be used in a selector requirement. + enum: + - In + - NotIn + - Exists + - DoesNotExist + type: string + values: + items: + maxLength: 63 + pattern: ^([A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?)?$ + type: string + maxItems: 64 + type: array + required: + - key + - operator + type: object + x-kubernetes-validations: + - message: selector operator requires compatible values + rule: '(self.operator in [''In'', ''NotIn'']) ? (has(self.values) + && self.values.size() > 0) : (!has(self.values) || + self.values.size() == 0)' + maxItems: 64 + type: array + matchLabels: + additionalProperties: + type: string + maxProperties: 64 + type: object + type: object required: - dnsZoneRef - namePrefix - - registrationClass type: object x-kubernetes-validations: - message: dnsZoneRef must pin the zone UID @@ -82,8 +118,9 @@ spec: 0 type: array dnsZoneRef: - description: Legacy single-rule fields are retained during the v1alpha1 - transition. + 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 @@ -105,8 +142,6 @@ spec: priority: format: int32 type: integer - product: - type: string resolverContextRef: description: |- DNSObjectReference pins a namespaced object by API-assigned UID. Generation @@ -131,43 +166,11 @@ spec: type: object scopePrefix: type: string - vpcRef: - 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: - additionalNames + - resolverContextRef type: object x-kubernetes-validations: - - message: exactly one UID-pinned vpcRef or resolverContextRef is required - rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() - > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) - && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() - > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() - > 0) - - message: a nonempty vpcRef must pin name and UID - rule: '!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) - && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) - || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() - > 0))' - message: a nonempty resolverContextRef must pin name and UID rule: '!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) @@ -177,6 +180,9 @@ spec: - message: a selected dnsZoneRef must pin the zone UID rule: '!has(self.dnsZoneRef) || self.dnsZoneRef.name.size() == 0 || (has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0)' + - message: resolverContextRef is required + rule: self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) + && self.resolverContextRef.uid.size() > 0 status: properties: conditions: @@ -262,17 +268,58 @@ spec: type: object namePrefix: type: string - registrationClass: - enum: - - InstanceIdentity - - ServiceDiscovery - - ServiceVIP - - ServiceExport - type: string + selector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - dnsZoneRef - namePrefix - - registrationClass type: object type: array resolvedDNSZoneRef: @@ -319,28 +366,6 @@ spec: required: - name type: object - resolvedVPCRef: - 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 type: object required: - spec diff --git a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml index 8cbd7304..36b832a2 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml @@ -84,43 +84,11 @@ spec: required: - name type: object - vpcRef: - 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: - dnsZoneRef + - resolverContextRef type: object x-kubernetes-validations: - - message: exactly one UID-pinned vpcRef or resolverContextRef is required - rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() - > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() > 0) != (has(self.resolverContextRef) - && has(self.resolverContextRef.name) && self.resolverContextRef.name.size() - > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() - > 0) - - message: a nonempty vpcRef must pin name and UID - rule: '!has(self.vpcRef) || ((self.vpcRef.name.size() == 0 && !has(self.vpcRef.uid) - && (!has(self.vpcRef.generation) || self.vpcRef.generation == 0)) - || (self.vpcRef.name.size() > 0 && has(self.vpcRef.uid) && self.vpcRef.uid.size() - > 0))' - message: a nonempty resolverContextRef must pin name and UID rule: '!has(self.resolverContextRef) || ((self.resolverContextRef.name.size() == 0 && !has(self.resolverContextRef.uid) && (!has(self.resolverContextRef.generation) @@ -129,6 +97,9 @@ spec: > 0))' - message: dnsZoneRef must pin the zone UID rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 + - message: resolverContextRef is required + rule: self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) + && self.resolverContextRef.uid.size() > 0 status: properties: bindingRef: @@ -253,28 +224,6 @@ spec: required: - name type: object - resolvedVPCRef: - 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 type: object required: - spec diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index f625740c..92f53338 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -31,13 +31,12 @@ const Path = "/validate-internal-dns" // publisher. For project-aware deployments, ClientForRequest must resolve the // authenticated project context and return its immutable source cluster UID. type Handler struct { - Client client.Client - TrustedClusterUID string - MaxContributionLease time.Duration - MaxAccessLease time.Duration - ResolverContextsEnabled bool - Now func() time.Time - ClientForRequest func(context.Context, kubeadmission.Request) (client.Client, string, error) + Client client.Client + TrustedClusterUID string + MaxContributionLease time.Duration + MaxAccessLease time.Duration + Now func() time.Time + ClientForRequest func(context.Context, kubeadmission.Request) (client.Client, string, error) } var _ kubeadmission.Handler = (*Handler)(nil) @@ -259,14 +258,9 @@ func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadm if _, err := currentPrivateZone(ctx, cl, req.Namespace, a.Spec.DNSZoneRef); err != nil { return err } - if err := validateConsumerSelector(a.Spec.VPCRef, a.Spec.ResolverContextRef, h.ResolverContextsEnabled); err != nil { - return err - } - if h.ResolverContextsEnabled { - if a.Spec.ResolverContextRef.UID == "" { - return fmt.Errorf("association must pin resolver context UID") - } - return nil + + if a.Spec.ResolverContextRef.UID == "" { + return fmt.Errorf("association must pin resolver context UID") } return nil } @@ -276,44 +270,31 @@ func (h *Handler) namingPolicy(ctx context.Context, cl client.Client, req kubead if err := decode(req, &p); err != nil { return err } - if err := validateConsumerSelector(p.Spec.VPCRef, p.Spec.ResolverContextRef, h.ResolverContextsEnabled); err != nil { - return err - } + if p.Spec.DNSZoneRef.Name != "" { if _, err := currentPrivateZone(ctx, cl, req.Namespace, p.Spec.DNSZoneRef); err != nil { return err } } for _, rule := range p.Spec.AdditionalNames { + if _, err := metav1.LabelSelectorAsSelector(rule.Selector.AsLabelSelector()); err != nil { + return fmt.Errorf("invalid naming selector: %w", err) + } if _, err := currentPrivateZone(ctx, cl, req.Namespace, rule.DNSZoneRef); err != nil { return err } } - if h.ResolverContextsEnabled { - if p.Spec.ResolverContextRef.Name == "" || p.Spec.ResolverContextRef.UID == "" { - return fmt.Errorf("naming policy must pin resolver context name and UID") - } - var resolverContext dnsv1alpha1.DNSResolverContext - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: p.Spec.ResolverContextRef.Name}, &resolverContext); err != nil { - return fmt.Errorf("resolver context lookup: %w", err) - } - ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") - if resolverContext.UID != p.Spec.ResolverContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() || ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { - return fmt.Errorf("resolver context lifetime is not ready") - } - return nil - } - return nil -} -func validateConsumerSelector(vpcRef, contextRef dnsv1alpha1.DNSObjectReference, contextMode bool) error { - hasVPC := vpcRef.Name != "" || vpcRef.UID != "" || vpcRef.Generation != 0 - hasContext := contextRef.Name != "" || contextRef.UID != "" || contextRef.Generation != 0 - if contextMode && !hasContext { - return fmt.Errorf("resolver context mode requires resolverContextRef") - } - if !contextMode && !hasVPC { - return fmt.Errorf("legacy networking mode requires vpcRef") + if p.Spec.ResolverContextRef.Name == "" || p.Spec.ResolverContextRef.UID == "" { + return fmt.Errorf("naming policy must pin resolver context name and UID") + } + var resolverContext dnsv1alpha1.DNSResolverContext + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: p.Spec.ResolverContextRef.Name}, &resolverContext); err != nil { + return fmt.Errorf("resolver context lookup: %w", err) + } + ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") + if resolverContext.UID != p.Spec.ResolverContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() || ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { + return fmt.Errorf("resolver context lifetime is not ready") } return nil } From e5130849494bd2860b20f12bcd22e65666894739 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 11:07:16 -0500 Subject: [PATCH 14/28] fix: Keep DNS API state persistent --- api/v1alpha1/internaldns_access_test.go | 40 ++++-------- api/v1alpha1/internaldns_project_types.go | 25 +------- .../internaldns_validation_envtest_test.go | 9 +-- api/v1alpha1/zz_generated.deepcopy.go | 7 +-- ...ng.miloapis.com_dnscontributiongrants.yaml | 4 -- ...g.miloapis.com_dnsrecordcontributions.yaml | 3 - ...iloapis.com_dnsresolveraccessbindings.yaml | 4 -- internal/internaldns/admission/handler.go | 63 ++++--------------- .../internaldns/admission/handler_test.go | 34 ++++------ internal/internaldns/model/model.go | 39 +++++------- internal/internaldns/model/model_test.go | 2 +- 11 files changed, 56 insertions(+), 174 deletions(-) diff --git a/api/v1alpha1/internaldns_access_test.go b/api/v1alpha1/internaldns_access_test.go index d4abda23..e67b646e 100644 --- a/api/v1alpha1/internaldns_access_test.go +++ b/api/v1alpha1/internaldns_access_test.go @@ -1,42 +1,24 @@ -// SPDX-License-Identifier: AGPL-3.0-only - package v1alpha1 import ( "encoding/json" "testing" - "time" ) -func TestAccessAuthorizationPreservesDeadlinePrecision(t *testing.T) { - const raw = `{"writerEpoch":1,"sequence":2,"validUntil":"2026-10-08T23:22:29.022357123Z"}` - var authorization DNSResolverAccessAuthorization - if err := json.Unmarshal([]byte(raw), &authorization); err != nil { +func TestResolverAccessHasNoExpiryOrClusterIdentity(t *testing.T) { + a := DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 2} + b, err := json.Marshal(a) + if err != nil { t.Fatal(err) } - for i := 0; i < 3; i++ { - encoded, err := json.Marshal(authorization) - if err != nil { - t.Fatal(err) - } - if string(encoded) != raw { - t.Fatalf("authorization deadline changed on round trip: %s", encoded) - } - if err := json.Unmarshal(encoded, &authorization); err != nil { - t.Fatal(err) - } - } - if authorization.ValidUntil.Nanosecond() != 22357123 { - t.Fatal("authorization lost nanosecond precision") + if string(b) != `{"writerEpoch":1,"sequence":2}` { + t.Fatalf("unexpected access fence: %s", b) } - // The same instant in another time zone serializes to the same UTC fence. - authorization.ValidUntil.Time = authorization.ValidUntil.In(time.FixedZone("offset", 3600)) - encoded, err := json.Marshal(authorization) - if err != nil || string(encoded) != raw { - t.Fatalf("UTC normalization changed authorization: %s, %v", encoded, err) + b, err = json.Marshal(DNSProducerPrincipal{Subject: "system:serviceaccount:compute:dns-publisher"}) + if err != nil { + t.Fatal(err) } - encoded, err = json.Marshal(DNSResolverAccessAuthorization{}) - if err != nil || string(encoded) != `{"writerEpoch":0,"sequence":0,"validUntil":null}` { - t.Fatalf("zero value encoding changed field names or null deadline: %s, %v", encoded, err) + if string(b) != `{"subject":"system:serviceaccount:compute:dns-publisher"}` { + t.Fatalf("unexpected publisher identity: %s", b) } } diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index ee23cc8d..deed727f 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -3,9 +3,6 @@ package v1alpha1 import ( - "encoding/json" - "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -102,24 +99,7 @@ type DNSResolverAccessAuthorization struct { // +kubebuilder:validation:Minimum=1 WriterEpoch int64 `json:"writerEpoch"` // +kubebuilder:validation:Minimum=1 - Sequence int64 `json:"sequence"` - ValidUntil metav1.Time `json:"validUntil"` -} - -// MarshalJSON preserves the source authorization deadline exactly. metav1.Time -// accepts fractional seconds on input but its default JSON encoder drops them, -// which would mutate a same-fence deadline when projecting an access binding. -func (a DNSResolverAccessAuthorization) MarshalJSON() ([]byte, error) { - var deadline *time.Time - if !a.ValidUntil.IsZero() { - instant := a.ValidUntil.UTC() - deadline = &instant - } - return json.Marshal(struct { - WriterEpoch int64 `json:"writerEpoch"` - Sequence int64 `json:"sequence"` - ValidUntil *time.Time `json:"validUntil"` - }{WriterEpoch: a.WriterEpoch, Sequence: a.Sequence, ValidUntil: deadline}) + Sequence int64 `json:"sequence"` } // +kubebuilder:validation:XValidation:rule="self.contextRef.name.size() > 0 && has(self.contextRef.uid) && self.contextRef.uid.size() > 0",message="contextRef requires name and UID" @@ -232,8 +212,6 @@ type DNSRegistrationList struct { } type DNSProducerPrincipal struct { - // +kubebuilder:validation:MinLength=1 - ClusterUID string `json:"clusterUID"` // +kubebuilder:validation:MinLength=1 Subject string `json:"subject"` } @@ -291,7 +269,6 @@ type DNSRecordContributionStatus struct { Sequence int64 `json:"sequence,omitempty"` Eligible bool `json:"eligible,omitempty"` Reason string `json:"reason,omitempty"` - ValidUntil *metav1.Time `json:"validUntil,omitempty"` PublishedRevision int64 `json:"publishedRevision,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index e9ae53d3..eeb8dd74 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "testing" - "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -57,7 +56,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { } newAccess := func(name string) *DNSResolverAccessBinding { return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ - ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 1, ValidUntil: metav1.NewTime(time.Now().Add(time.Minute))}, + ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 1}, }} } for _, tc := range []struct { @@ -86,9 +85,6 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { {"change-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "east" }}, {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "fd53::2" }}, {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.WriterEpoch++ }}, - {"extend-without-sequence", func(a *DNSResolverAccessBinding) { - a.Spec.Authorization.ValidUntil = metav1.NewTime(a.Spec.Authorization.ValidUntil.Add(time.Minute)) - }}, {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.Sequence = 1 }}, } { t.Run(tc.name, func(t *testing.T) { @@ -101,13 +97,12 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { requireInvalid(t, cl.Update(ctx, a)) }) } - t.Run("renewal-and-status", func(t *testing.T) { + t.Run("sequence-and-status", func(t *testing.T) { a := newAccess("renewal") if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } a.Spec.Authorization.Sequence++ - a.Spec.Authorization.ValidUntil = metav1.NewTime(a.Spec.Authorization.ValidUntil.Add(time.Minute)) if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8c13b75b..fca408f5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -567,10 +567,6 @@ func (in *DNSRecordContributionSpec) DeepCopy() *DNSRecordContributionSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordContributionStatus) DeepCopyInto(out *DNSRecordContributionStatus) { *out = *in - if in.ValidUntil != nil { - in, out := &in.ValidUntil, &out.ValidUntil - *out = (*in).DeepCopy() - } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) @@ -881,7 +877,6 @@ func (in *DNSResolvedAdditionalNameRule) DeepCopy() *DNSResolvedAdditionalNameRu // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSResolverAccessAuthorization) DeepCopyInto(out *DNSResolverAccessAuthorization) { *out = *in - in.ValidUntil.DeepCopyInto(&out.ValidUntil) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessAuthorization. @@ -963,7 +958,7 @@ func (in *DNSResolverAccessBindingSpec) DeepCopyInto(out *DNSResolverAccessBindi *out = make([]string, len(*in)) copy(*out, *in) } - in.Authorization.DeepCopyInto(&out.Authorization) + out.Authorization = in.Authorization } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingSpec. diff --git a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml index 46a238eb..3614543d 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml @@ -44,14 +44,10 @@ spec: type: array principal: properties: - clusterUID: - minLength: 1 - type: string subject: minLength: 1 type: string required: - - clusterUID - subject type: object producerID: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 163f509d..0f38938b 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -412,9 +412,6 @@ spec: sequence: format: int64 type: integer - validUntil: - format: date-time - type: string writerEpoch: format: int64 type: integer diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 99cd47bf..f743b3d3 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -44,16 +44,12 @@ spec: format: int64 minimum: 1 type: integer - validUntil: - format: date-time - type: string writerEpoch: format: int64 minimum: 1 type: integer required: - sequence - - validUntil - writerEpoch type: object contextRef: diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 92f53338..987f0eb4 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -11,7 +11,6 @@ import ( "fmt" "net/http" "reflect" - "time" dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" "go.miloapis.com/dns-operator/internal/internaldns/model" @@ -26,17 +25,11 @@ const contributionResource = "dnsrecordcontributions" const Path = "/validate-internal-dns" -// Handler uses an uncached project client for grant and registration lookups. -// TrustedClusterUID is supplied by the server configuration, never by a record -// publisher. For project-aware deployments, ClientForRequest must resolve the -// authenticated project context and return its immutable source cluster UID. +// Handler uses an uncached client selected by authenticated project context. type Handler struct { - Client client.Client - TrustedClusterUID string - MaxContributionLease time.Duration - MaxAccessLease time.Duration - Now func() time.Time - ClientForRequest func(context.Context, kubeadmission.Request) (client.Client, string, error) + Client client.Client + + ClientForRequest func(context.Context, kubeadmission.Request) (client.Client, error) } var _ kubeadmission.Handler = (*Handler)(nil) @@ -54,14 +47,14 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm } switch req.Resource.Resource { case "dnsregistrations", contributionResource, "dnscontributiongrants", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": - // These resources require live references or bounded freshness checks. + // These resources require live references. default: return kubeadmission.Allowed("API server authorization and schema validation apply") } - cl, clusterUID := h.Client, h.TrustedClusterUID + cl := h.Client if h.ClientForRequest != nil { var err error - cl, clusterUID, err = h.ClientForRequest(ctx, req) + cl, err = h.ClientForRequest(ctx, req) if err != nil { return kubeadmission.Errored(http.StatusServiceUnavailable, err) } @@ -76,7 +69,7 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm case "dnsregistrations": err = h.registration(ctx, cl, req) case contributionResource: - err = h.contribution(ctx, cl, clusterUID, req) + err = h.contribution(ctx, cl, req) case "dnscontributiongrants": err = h.grant(ctx, cl, req) case "dnszoneassociations": @@ -97,7 +90,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if err := decode(req, ¤t); err != nil { return err } - a := current.Spec.Authorization var resolverContext dnsv1alpha1.DNSResolverContext if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: current.Spec.ContextRef.Name}, &resolverContext); err != nil { return fmt.Errorf("resolver context lookup: %w", err) @@ -112,24 +104,10 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { return fmt.Errorf("resolver context is not ready for its current generation") } - max := h.MaxAccessLease - if max <= 0 { - max = 5 * time.Minute - } - now := time.Now() - if h.Now != nil { - now = h.Now() - } - if !a.ValidUntil.After(now) { - return fmt.Errorf("access deadline is already expired") - } - if a.ValidUntil.After(now.Add(max)) { - return fmt.Errorf("access deadline exceeds platform lease limit") - } return nil } -func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID string, req kubeadmission.Request) error { +func (h *Handler) contribution(ctx context.Context, cl client.Client, req kubeadmission.Request) error { var c, old dnsv1alpha1.DNSRecordContribution if err := decode(req, &c); err != nil { return err @@ -147,8 +125,8 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if reflect.DeepEqual(previous, current) { return nil } - // Binding an issued epoch cannot introduce or renew endpoint eligibility. - if previous.WriterEpoch == 0 && previous.ObservedGeneration == 0 && previous.Sequence == 0 && !previous.Eligible && previous.ValidUntil == nil && current.WriterEpoch > 0 { + // Binding an issued epoch cannot introduce endpoint eligibility. + if previous.WriterEpoch == 0 && previous.ObservedGeneration == 0 && previous.Sequence == 0 && !previous.Eligible && current.WriterEpoch > 0 { current.WriterEpoch = 0 bindEpoch = reflect.DeepEqual(previous, current) } @@ -160,7 +138,7 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if g.UID != c.Spec.GrantRef.UID || !g.DeletionTimestamp.IsZero() { return fmt.Errorf("grant lifetime is not current") } - if clusterUID == "" || g.Spec.Principal.ClusterUID != clusterUID || (!bindEpoch && g.Spec.Principal.Subject != req.UserInfo.Username) { + if !bindEpoch && g.Spec.Principal.Subject != req.UserInfo.Username { return fmt.Errorf("caller does not match the grant's authenticated producer principal") } if g.Spec.RegistrationRef.UID != c.Spec.RegistrationRef.UID || g.Spec.RegistrationRef.Name != c.Spec.RegistrationRef.Name { @@ -212,23 +190,6 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, clusterUID if c.Status.ObservedGeneration != c.Generation { return fmt.Errorf("producer observation must match the contribution generation") } - if c.Status.ValidUntil == nil { - return fmt.Errorf("producer observation requires an original freshness deadline") - } - now := time.Now() - if h.Now != nil { - now = h.Now() - } - maxLease := h.MaxContributionLease - if maxLease <= 0 { - maxLease = 90 * time.Second - } - if c.Status.ValidUntil.After(now.Add(maxLease)) { - return fmt.Errorf("freshness deadline exceeds platform lease limit") - } - if c.Status.Eligible && !c.Status.ValidUntil.After(now) { - return fmt.Errorf("eligible observation is already expired") - } } else if req.Operation != admissionv1.Delete && !reflect.DeepEqual(old.Status, c.Status) && req.Operation != admissionv1.Create { return fmt.Errorf("producer observations must use the status subresource") } diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 02996320..3d36e8f1 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -20,7 +20,6 @@ import ( ) func TestContributionAuthenticatedWriter(t *testing.T) { - now := time.Date(2026, 10, 6, 18, 0, 0, 0, time.UTC) producer := "system:serviceaccount:compute-system:dns-publisher" registration := &dnsv1alpha1.DNSRegistration{ ObjectMeta: metav1.ObjectMeta{Name: "api", Namespace: "project", UID: "reg-uid", Generation: 3}, @@ -30,7 +29,7 @@ func TestContributionAuthenticatedWriter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "compute", Namespace: "project", UID: "grant-uid"}, Spec: dnsv1alpha1.DNSContributionGrantSpec{ RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid"}, - Principal: dnsv1alpha1.DNSProducerPrincipal{ClusterUID: "cluster-uid", Subject: producer}, + Principal: dnsv1alpha1.DNSProducerPrincipal{Subject: producer}, RecordTypes: []dnsv1alpha1.RRType{"AAAA"}, }, Status: dnsv1alpha1.DNSContributionGrantStatus{ActiveWriterEpoch: 8, ObservedRegistrationGeneration: 3}, @@ -41,13 +40,13 @@ func TestContributionAuthenticatedWriter(t *testing.T) { RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid", Generation: 3}, GrantRef: dnsv1alpha1.DNSObjectReference{Name: "compute", UID: "grant-uid"}, }, - Status: dnsv1alpha1.DNSRecordContributionStatus{ObservedGeneration: 2, WriterEpoch: 8, Sequence: 18, Eligible: true, ValidUntil: ptrTime(now.Add(30 * time.Second))}, + Status: dnsv1alpha1.DNSRecordContributionStatus{ObservedGeneration: 2, WriterEpoch: 8, Sequence: 18, Eligible: true}, } scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } - h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(registration, grant, &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}}).Build(), TrustedClusterUID: "cluster-uid", Now: func() time.Time { return now }} + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(registration, grant, &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}}).Build()} cases := []struct { name string user string @@ -69,8 +68,6 @@ func TestContributionAuthenticatedWriter(t *testing.T) { {"forged grant UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.GrantRef.UID = "other-grant" }, false}, {"retired registration UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.RegistrationRef.UID = "old-reg" }, false}, {"publication fields", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.PublishedRevision = 1000 }, false}, - {"overlong lease", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ValidUntil = ptrTime(now.Add(time.Hour)) }, false}, - {"expired eligible lease", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ValidUntil = ptrTime(now.Add(-time.Second)) }, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -116,8 +113,6 @@ func TestUnavailableProjectContextFailsClosed(t *testing.T) { } } -func ptrTime(t time.Time) *metav1.Time { v := metav1.NewTime(t); return &v } - func request(resource, username string, obj, old any) kubeadmission.Request { raw, _ := json.Marshal(obj) var oldRaw []byte @@ -159,7 +154,7 @@ func TestRetiredProductIntentCanBeDeletedWithoutLiveGrant(t *testing.T) { } } -func TestResolverAccessRequiresCurrentContextAndBoundedLease(t *testing.T) { +func TestResolverAccessRequiresCurrentContext(t *testing.T) { now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { @@ -167,20 +162,13 @@ func TestResolverAccessRequiresCurrentContextAndBoundedLease(t *testing.T) { } resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessWriterEpoch: 3}} setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) - h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build(), MaxAccessLease: 5 * time.Minute, Now: func() time.Time { return now }} - old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: dnsv1alpha1.DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 9, ValidUntil: metav1.NewTime(now.Add(2 * time.Minute))}}} + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build()} + old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: dnsv1alpha1.DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 9}}} current := old.DeepCopy() current.Spec.Authorization.Sequence = 10 - current.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(3 * time.Minute)) req := request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if res := h.Handle(context.Background(), req); !res.Allowed { - t.Fatalf("fresh integration renewal denied: %v", res.Result) - } - overlong := current.DeepCopy() - overlong.Spec.Authorization.ValidUntil = metav1.NewTime(now.Add(6 * time.Minute)) - req = request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", overlong, old) - if h.Handle(context.Background(), req).Allowed { - t.Fatal("overlong access lease accepted") + t.Fatalf("current integration update denied: %v", res.Result) } forged := current.DeepCopy() forged.Spec.ContextRef.UID = "replacement" @@ -242,9 +230,9 @@ func TestRegistrationRequiresCurrentZone(t *testing.T) { } } -func TestPublicationStatusDoesNotRenewPublisherObservation(t *testing.T) { +func TestPublicationStatusDoesNotChangePublisherObservation(t *testing.T) { h := Handler{Client: fake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()} - old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 4, Sequence: 10, Eligible: true, ValidUntil: ptrTime(time.Now().Add(-time.Minute))}} + old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 4, Sequence: 10, Eligible: true}} current := old.DeepCopy() current.Status.PublishedRevision = 12 current.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue}} @@ -253,10 +241,10 @@ func TestPublicationStatusDoesNotRenewPublisherObservation(t *testing.T) { if result := h.Handle(context.Background(), req); !result.Allowed { t.Fatalf("publication result needs no producer grant: %v", result.Result) } - current.Status.ValidUntil = ptrTime(time.Now().Add(time.Minute)) + current.Status.Sequence++ req = request("dnsrecordcontributions", "rbac-authorized-controller", current, old) req.SubResource = "status" if h.Handle(context.Background(), req).Allowed { - t.Fatal("publication update renewed producer freshness without a grant") + t.Fatal("publication update changed the producer observation without a grant") } } diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index 2f65c3c9..272e7cda 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -17,7 +17,7 @@ import ( "github.com/miekg/dns" ) -const APIVersion = "internal.dns.networking.miloapis.com/v1alpha2" +const APIVersion = "internal.dns.networking.miloapis.com/v1alpha4" type Kind string @@ -78,9 +78,8 @@ const ( ) type Authorization struct { - IssuerEpoch uint64 `json:"issuerEpoch"` - Revision uint64 `json:"revision"` - ValidUntil time.Time `json:"validUntil"` + IssuerEpoch uint64 `json:"issuerEpoch"` + Revision uint64 `json:"revision"` } type Backend struct { @@ -189,7 +188,7 @@ func (s ServingSnapshot) Validate() error { } seenVIP[raw] = b.BindingUID } - if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 || b.Authorization.ValidUntil.IsZero() { + if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 { return fmt.Errorf("binding %q has incomplete transport or authorization", b.BindingUID) } if err := validateTransports(b.Transports); err != nil { @@ -317,12 +316,11 @@ func (c PublicationChunk) Validate() error { } type RRRecord struct { - Content string `json:"content"` - ContributionUID string `json:"contributionUID,omitempty"` - WriterEpoch uint64 `json:"writerEpoch,omitempty"` - Sequence uint64 `json:"sequence,omitempty"` - Eligible bool `json:"eligible"` - ValidUntil time.Time `json:"validUntil,omitempty"` + Content string `json:"content"` + ContributionUID string `json:"contributionUID,omitempty"` + WriterEpoch uint64 `json:"writerEpoch,omitempty"` + Sequence uint64 `json:"sequence,omitempty"` + Eligible bool `json:"eligible"` } type RRSet struct { @@ -346,15 +344,12 @@ type RegistrationFence struct { Generation uint64 `json:"generation"` } -// ObservationFence carries the accepted high-water mark even when an -// unhealthy or withdrawn observation produces no records. ValidUntil is the -// original contributor deadline and is part of the immutable fence identity. +// ObservationFence carries the accepted high-water mark, including withdrawn observations. type ObservationFence struct { - ContributionUID string `json:"contributionUID"` - GrantUID string `json:"grantUID"` - WriterEpoch uint64 `json:"writerEpoch"` - Sequence uint64 `json:"sequence"` - ValidUntil time.Time `json:"validUntil"` + ContributionUID string `json:"contributionUID"` + GrantUID string `json:"grantUID"` + WriterEpoch uint64 `json:"writerEpoch"` + Sequence uint64 `json:"sequence"` } func (p PublicationPlan) Validate() error { @@ -378,7 +373,7 @@ func (p PublicationPlan) Validate() error { observationByUID := make(map[string]ObservationFence, len(p.ObservationFences)) previousContribution := "" for _, observation := range p.ObservationFences { - if observation.ContributionUID == "" || observation.GrantUID == "" || observation.WriterEpoch == 0 || observation.Sequence == 0 || observation.ValidUntil.IsZero() { + if observation.ContributionUID == "" || observation.GrantUID == "" || observation.WriterEpoch == 0 || observation.Sequence == 0 { return errors.New("publication observation fence is incomplete") } if previousContribution != "" && observation.ContributionUID <= previousContribution { @@ -416,12 +411,12 @@ func (p PublicationPlan) Validate() error { return fmt.Errorf("RRset %q contains an invalid record", key) } if rec.ContributionUID == "" { - if rec.WriterEpoch != 0 || rec.Sequence != 0 || !rec.ValidUntil.IsZero() { + if rec.WriterEpoch != 0 || rec.Sequence != 0 { return fmt.Errorf("RRset %q static record contains contribution fence data", key) } } else { observation, ok := observationByUID[rec.ContributionUID] - if !ok || rec.WriterEpoch != observation.WriterEpoch || rec.Sequence != observation.Sequence || !rec.ValidUntil.Equal(observation.ValidUntil) { + if !ok || rec.WriterEpoch != observation.WriterEpoch || rec.Sequence != observation.Sequence { return fmt.Errorf("RRset %q record fence does not match contribution observation %q", key, rec.ContributionUID) } } diff --git a/internal/internaldns/model/model_test.go b/internal/internaldns/model/model_test.go index ef88d0fb..82b6ad87 100644 --- a/internal/internaldns/model/model_test.go +++ b/internal/internaldns/model/model_test.go @@ -42,7 +42,7 @@ func TestServingSnapshotRejectsSharedDestination(t *testing.T) { t.Parallel() now := time.Now() b := func(uid, vpc string) Binding { - return Binding{BindingUID: uid, ProjectUID: "project", ContextUID: vpc, BindingGeneration: 1, ConfigurationRevision: 1, ConsumerAddress: "2001:db8::1", ClusterAddress: "2001:db8::2", Port: 53, Transports: []Transport{TransportUDP, TransportTCP}, Authorization: Authorization{IssuerEpoch: 1, Revision: 1, ValidUntil: now.Add(time.Minute)}} + return Binding{BindingUID: uid, ProjectUID: "project", ContextUID: vpc, BindingGeneration: 1, ConfigurationRevision: 1, ConsumerAddress: "2001:db8::1", ClusterAddress: "2001:db8::2", Port: 53, Transports: []Transport{TransportUDP, TransportTCP}, Authorization: Authorization{IssuerEpoch: 1, Revision: 1}} } s := ServingSnapshot{Region: "east", Shard: "s1", ConfigurationEpoch: 1, ConfigurationRevision: 1, GeneratedAt: now, Bindings: []Binding{b("one", "vpc-a"), b("two", "vpc-b")}} if err := s.Validate(); err == nil { From a77ed35cb55b35960d255121f790294774cafaa6 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 11:21:42 -0500 Subject: [PATCH 15/28] refactor: Consolidate DNS continuity contracts --- api/v1alpha1/internaldns_service_types.go | 3 +++ api/v1alpha1/zz_generated.deepcopy.go | 7 +++++++ .../dns.networking.miloapis.com_dnsresolverbindings.yaml | 7 +++++++ internal/internaldns/model/model.go | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/api/v1alpha1/internaldns_service_types.go b/api/v1alpha1/internaldns_service_types.go index dc57e565..f00fc424 100644 --- a/api/v1alpha1/internaldns_service_types.go +++ b/api/v1alpha1/internaldns_service_types.go @@ -50,6 +50,9 @@ type DNSResolverBindingConfiguration struct { Listeners DNSResolverBindingListeners `json:"listeners"` // +kubebuilder:validation:MaxItems=256 ZoneRefs []DNSObjectReference `json:"zoneRefs"` + // ZoneApices reserves each private namespace before publication is available. + // +kubebuilder:validation:MaxProperties=256 + ZoneApices map[string]string `json:"zoneApices,omitempty"` } // +kubebuilder:validation:XValidation:rule="oldSelf == self",message="listener identity is immutable for an access binding" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6a5b004c..c69e6921 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1401,6 +1401,13 @@ func (in *DNSResolverBindingConfiguration) DeepCopyInto(out *DNSResolverBindingC *out = make([]DNSObjectReference, len(*in)) copy(*out, *in) } + if in.ZoneApices != nil { + in, out := &in.ZoneApices, &out.ZoneApices + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverBindingConfiguration. diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml index 4d78b627..81137d8c 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolverbindings.yaml @@ -124,6 +124,13 @@ spec: format: int64 minimum: 1 type: integer + zoneApices: + additionalProperties: + type: string + description: ZoneApices reserves each private namespace before + publication is available. + maxProperties: 256 + type: object zoneRefs: items: description: |- diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index 272e7cda..b6f1e5dd 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -89,6 +89,7 @@ type Backend struct { } type ZoneAttachment struct { + Pending bool `json:"pending,omitempty"` ZoneUID string `json:"zoneUID"` Apex string `json:"apex"` RequiredPublicationEpoch uint64 `json:"requiredPublicationEpoch"` @@ -111,6 +112,9 @@ type Binding struct { ClusterBackends []Backend `json:"clusterBackends"` Authorization Authorization `json:"authorization"` Tombstone bool `json:"tombstone,omitempty"` + // ConfigurationPending gates queries while attached publications are absent. + // It does not revoke or advance the source authorization. + ConfigurationPending bool `json:"configurationPending,omitempty"` } func (b Binding) ViewName() string { @@ -191,6 +195,9 @@ func (s ServingSnapshot) Validate() error { if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 { return fmt.Errorf("binding %q has incomplete transport or authorization", b.BindingUID) } + if b.ConfigurationPending && len(b.Zones) != 0 { + return fmt.Errorf("binding %q pending configuration must not expose partial zones", b.BindingUID) + } if err := validateTransports(b.Transports); err != nil { return fmt.Errorf("binding %q: %w", b.BindingUID, err) } From d4beb2fea949e942627394ae0371e6ec6824afc2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:29:26 -0500 Subject: [PATCH 16/28] refactor: Simplify private DNS publication APIs --- api/v1alpha1/internaldns_access_test.go | 11 +- api/v1alpha1/internaldns_project_types.go | 81 +++----- .../internaldns_validation_envtest_test.go | 52 +++-- api/v1alpha1/zz_generated.deepcopy.go | 156 ++------------- ...ng.miloapis.com_dnscontributiongrants.yaml | 180 ------------------ ...g.miloapis.com_dnsrecordcontributions.yaml | 44 ++--- ...iloapis.com_dnsresolveraccessbindings.yaml | 39 ++-- ...king.miloapis.com_dnsresolvercontexts.yaml | 2 +- config/crd/kustomization.yaml | 1 - .../iam/protected-resources/internal-dns.yaml | 18 -- internal/internaldns/admission/handler.go | 120 +++--------- .../internaldns/admission/handler_test.go | 108 +++-------- internal/internaldns/model/model.go | 25 ++- internal/internaldns/model/model_test.go | 2 +- 14 files changed, 175 insertions(+), 664 deletions(-) delete mode 100644 config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml diff --git a/api/v1alpha1/internaldns_access_test.go b/api/v1alpha1/internaldns_access_test.go index e67b646e..887b0234 100644 --- a/api/v1alpha1/internaldns_access_test.go +++ b/api/v1alpha1/internaldns_access_test.go @@ -6,19 +6,12 @@ import ( ) func TestResolverAccessHasNoExpiryOrClusterIdentity(t *testing.T) { - a := DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 2} + a := DNSResolverAccessVersion{Epoch: 1, Sequence: 2} b, err := json.Marshal(a) if err != nil { t.Fatal(err) } - if string(b) != `{"writerEpoch":1,"sequence":2}` { + if string(b) != `{"epoch":1,"sequence":2}` { t.Fatalf("unexpected access fence: %s", b) } - b, err = json.Marshal(DNSProducerPrincipal{Subject: "system:serviceaccount:compute:dns-publisher"}) - if err != nil { - t.Fatal(err) - } - if string(b) != `{"subject":"system:serviceaccount:compute:dns-publisher"}` { - t.Fatalf("unexpected publisher identity: %s", b) - } } diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index deed727f..6cb1cbd0 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -55,10 +55,10 @@ type DNSResolverContextManagedNamespaceStatus struct { } type DNSResolverContextStatus struct { - ManagedNamespace DNSResolverContextManagedNamespaceStatus `json:"managedNamespace,omitempty"` - AccessWriterEpoch int64 `json:"accessWriterEpoch,omitempty"` - ServingTargets []DNSResolverContextServingTarget `json:"servingTargets,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + ManagedNamespace DNSResolverContextManagedNamespaceStatus `json:"managedNamespace,omitempty"` + AccessEpoch int64 `json:"accessEpoch,omitempty"` + ServingTargets []DNSResolverContextServingTarget `json:"servingTargets,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } type DNSResolverContextServingTarget struct { @@ -95,9 +95,9 @@ type DNSResolverQueryIdentity struct { Value string `json:"value"` } -type DNSResolverAccessAuthorization struct { +type DNSResolverAccessVersion struct { // +kubebuilder:validation:Minimum=1 - WriterEpoch int64 `json:"writerEpoch"` + Epoch int64 `json:"epoch"` // +kubebuilder:validation:Minimum=1 Sequence int64 `json:"sequence"` } @@ -105,8 +105,8 @@ type DNSResolverAccessAuthorization struct { // +kubebuilder:validation:XValidation:rule="self.contextRef.name.size() > 0 && has(self.contextRef.uid) && self.contextRef.uid.size() > 0",message="contextRef requires name and UID" // +kubebuilder:validation:XValidation:rule="self.contextRef == oldSelf.contextRef",message="contextRef is immutable" // +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity && self.port == oldSelf.port && self.transports == oldSelf.transports",message="access identity and transport are immutable" -// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch == oldSelf.authorization.writerEpoch",message="writerEpoch is immutable for an access binding" -// +kubebuilder:validation:XValidation:rule="self.authorization == oldSelf.authorization || self.authorization.sequence > oldSelf.authorization.sequence",message="authorization changes require an increasing sequence" +// +kubebuilder:validation:XValidation:rule="self.version.epoch == oldSelf.version.epoch",message="epoch is immutable for an access binding" +// +kubebuilder:validation:XValidation:rule="self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence",message="version changes require an increasing sequence" type DNSResolverAccessBindingSpec struct { ContextRef DNSObjectReference `json:"contextRef"` // +kubebuilder:validation:MinLength=1 @@ -119,8 +119,8 @@ type DNSResolverAccessBindingSpec struct { // +kubebuilder:validation:MaxItems=2 // +kubebuilder:validation:items:Enum=UDP;TCP // +listType=set - Transports []string `json:"transports"` - Authorization DNSResolverAccessAuthorization `json:"authorization"` + Transports []string `json:"transports"` + Version DNSResolverAccessVersion `json:"version"` } type DNSResolverAccessBindingStatus struct { @@ -211,66 +211,29 @@ type DNSRegistrationList struct { Items []DNSRegistration `json:"items"` } -type DNSProducerPrincipal struct { - // +kubebuilder:validation:MinLength=1 - Subject string `json:"subject"` -} - -// +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0",message="registrationRef requires name and UID" -type DNSContributionGrantSpec struct { - RegistrationRef DNSObjectReference `json:"registrationRef"` - ProducerID string `json:"producerID"` - Principal DNSProducerPrincipal `json:"principal"` - RecordTypes []RRType `json:"recordTypes"` - NameScopes []string `json:"nameScopes,omitempty"` -} - -type DNSContributionGrantStatus struct { - ActiveWriterEpoch int64 `json:"activeWriterEpoch,omitempty"` - ObservedGrantGeneration int64 `json:"observedGrantGeneration,omitempty"` - ObservedRegistrationGeneration int64 `json:"observedRegistrationGeneration,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" -type DNSContributionGrant struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DNSContributionGrantSpec `json:"spec"` - Status DNSContributionGrantStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true -type DNSContributionGrantList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []DNSContributionGrant `json:"items"` -} - type DNSContributionRecordSet struct { RecordType RRType `json:"recordType"` Records []RecordEntry `json:"records"` } // +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0",message="registrationRef requires name, UID, and generation" -// +kubebuilder:validation:XValidation:rule="self.grantRef.name.size() > 0 && has(self.grantRef.uid) && self.grantRef.uid.size() > 0",message="grantRef requires name and UID" -// +kubebuilder:validation:XValidation:rule="self.registrationRef == oldSelf.registrationRef && self.grantRef == oldSelf.grantRef",message="contribution authority references are immutable" +// +kubebuilder:validation:XValidation:rule="self.registrationRef == oldSelf.registrationRef",message="contribution registration reference is immutable" type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` - GrantRef DNSObjectReference `json:"grantRef"` RecordSets []DNSContributionRecordSet `json:"recordSets"` } +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.sequence) || (has(self.sequence) && self.sequence >= oldSelf.sequence)",message="observation sequence must not decrease" +// +kubebuilder:validation:XValidation:rule="(has(self.eligible) && self.eligible) == (has(oldSelf.eligible) && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) || self.sequence > oldSelf.sequence))",message="eligibility changes require a newer observation sequence" type DNSRecordContributionStatus struct { - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - WriterEpoch int64 `json:"writerEpoch,omitempty"` - Sequence int64 `json:"sequence,omitempty"` - Eligible bool `json:"eligible,omitempty"` - Reason string `json:"reason,omitempty"` - PublishedRevision int64 `json:"publishedRevision,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + // +kubebuilder:validation:Minimum=1 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // +kubebuilder:validation:Minimum=1 + Sequence int64 `json:"sequence,omitempty"` + Eligible bool `json:"eligible,omitempty"` + Reason string `json:"reason,omitempty"` + PublishedRevision int64 `json:"publishedRevision,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -404,5 +367,5 @@ type DNSManagedNamespaceList struct { } func init() { - SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSContributionGrant{}, &DNSContributionGrantList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}, &DNSManagedNamespace{}, &DNSManagedNamespaceList{}) + SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}, &DNSManagedNamespace{}, &DNSManagedNamespaceList{}) } diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index eeb8dd74..fb4eb44e 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -56,7 +56,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { } newAccess := func(name string) *DNSResolverAccessBinding { return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ - ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: DNSResolverAccessAuthorization{WriterEpoch: 1, Sequence: 1}, + ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: DNSResolverAccessVersion{Epoch: 1, Sequence: 1}, }} } for _, tc := range []struct { @@ -72,8 +72,8 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { {"missing-tcp", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP"} }}, {"duplicate-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "UDP"} }}, {"unsupported-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "TLS"} }}, - {"zero-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.WriterEpoch = 0 }}, - {"zero-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.Sequence = 0 }}, + {"zero-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch = 0 }}, + {"zero-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 0 }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name); tc.mutate(a); requireInvalid(t, cl.Create(ctx, a)) }) } @@ -84,12 +84,12 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { {"change-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "other" }}, {"change-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "east" }}, {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "fd53::2" }}, - {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.WriterEpoch++ }}, - {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Authorization.Sequence = 1 }}, + {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch++ }}, + {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 1 }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name) - a.Spec.Authorization.Sequence = 2 + a.Spec.Version.Sequence = 2 if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } @@ -102,13 +102,13 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Spec.Authorization.Sequence++ + a.Spec.Version.Sequence++ if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } - a.Status.ObservedSequence = a.Spec.Authorization.Sequence + a.Status.ObservedSequence = a.Spec.Version.Sequence if err := cl.Status().Update(ctx, a); err != nil { - t.Fatalf("unchanged source authorization blocked status update: %v", err) + t.Fatalf("unchanged access version blocked status update: %v", err) } a.Labels = map[string]string{"test": "metadata"} if err := cl.Update(ctx, a); err != nil { @@ -131,12 +131,40 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { requireInvalid(t, cl.Create(ctx, a)) }) } - t.Run("contribution-authority", func(t *testing.T) { - a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "contribution", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, GrantRef: DNSObjectReference{Name: "grant", UID: "grant-uid"}, RecordSets: []DNSContributionRecordSet{}}} + t.Run("contribution-registration", func(t *testing.T) { + a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "contribution", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}}} if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Spec.GrantRef.Name = "other" + a.Spec.RegistrationRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) + t.Run("contribution-observation-order", func(t *testing.T) { + a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "observations", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}}} + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Status = DNSRecordContributionStatus{ObservedGeneration: a.Generation, Sequence: 2, Eligible: true} + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + decreasing := a.DeepCopy() + decreasing.Status.Sequence = 1 + requireInvalid(t, cl.Status().Update(ctx, decreasing)) + unchanged := a.DeepCopy() + unchanged.Status.Eligible = false + requireInvalid(t, cl.Status().Update(ctx, unchanged)) + a.Status.Sequence++ + a.Status.Eligible = false + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + a.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue, Reason: "Applied", LastTransitionTime: metav1.Now()}} + if err := cl.Status().Update(ctx, a); err != nil { + t.Fatal(err) + } + }) + validateServiceDNSContracts(t, cl) + validateNamingAPISchemas(t, cl) + } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fca408f5..066a3987 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -108,114 +108,6 @@ 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 *DNSContributionGrant) DeepCopyInto(out *DNSContributionGrant) { - *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 DNSContributionGrant. -func (in *DNSContributionGrant) DeepCopy() *DNSContributionGrant { - if in == nil { - return nil - } - out := new(DNSContributionGrant) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSContributionGrant) 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 *DNSContributionGrantList) DeepCopyInto(out *DNSContributionGrantList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DNSContributionGrant, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionGrantList. -func (in *DNSContributionGrantList) DeepCopy() *DNSContributionGrantList { - if in == nil { - return nil - } - out := new(DNSContributionGrantList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSContributionGrantList) 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 *DNSContributionGrantSpec) DeepCopyInto(out *DNSContributionGrantSpec) { - *out = *in - out.RegistrationRef = in.RegistrationRef - out.Principal = in.Principal - if in.RecordTypes != nil { - in, out := &in.RecordTypes, &out.RecordTypes - *out = make([]RRType, len(*in)) - copy(*out, *in) - } - if in.NameScopes != nil { - in, out := &in.NameScopes, &out.NameScopes - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSContributionGrantSpec. -func (in *DNSContributionGrantSpec) DeepCopy() *DNSContributionGrantSpec { - if in == nil { - return nil - } - out := new(DNSContributionGrantSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSContributionGrantStatus) DeepCopyInto(out *DNSContributionGrantStatus) { - *out = *in - 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 DNSContributionGrantStatus. -func (in *DNSContributionGrantStatus) DeepCopy() *DNSContributionGrantStatus { - if in == nil { - return nil - } - out := new(DNSContributionGrantStatus) - 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 @@ -466,21 +358,6 @@ 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 *DNSProducerPrincipal) DeepCopyInto(out *DNSProducerPrincipal) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSProducerPrincipal. -func (in *DNSProducerPrincipal) DeepCopy() *DNSProducerPrincipal { - if in == nil { - return nil - } - out := new(DNSProducerPrincipal) - 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 @@ -544,7 +421,6 @@ func (in *DNSRecordContributionList) DeepCopyObject() runtime.Object { func (in *DNSRecordContributionSpec) DeepCopyInto(out *DNSRecordContributionSpec) { *out = *in out.RegistrationRef = in.RegistrationRef - out.GrantRef = in.GrantRef if in.RecordSets != nil { in, out := &in.RecordSets, &out.RecordSets *out = make([]DNSContributionRecordSet, len(*in)) @@ -874,21 +750,6 @@ func (in *DNSResolvedAdditionalNameRule) DeepCopy() *DNSResolvedAdditionalNameRu return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSResolverAccessAuthorization) DeepCopyInto(out *DNSResolverAccessAuthorization) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessAuthorization. -func (in *DNSResolverAccessAuthorization) DeepCopy() *DNSResolverAccessAuthorization { - if in == nil { - return nil - } - out := new(DNSResolverAccessAuthorization) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSResolverAccessBinding) DeepCopyInto(out *DNSResolverAccessBinding) { *out = *in @@ -958,7 +819,7 @@ func (in *DNSResolverAccessBindingSpec) DeepCopyInto(out *DNSResolverAccessBindi *out = make([]string, len(*in)) copy(*out, *in) } - out.Authorization = in.Authorization + out.Version = in.Version } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingSpec. @@ -998,6 +859,21 @@ func (in *DNSResolverAccessBindingStatus) DeepCopy() *DNSResolverAccessBindingSt return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSResolverAccessVersion) DeepCopyInto(out *DNSResolverAccessVersion) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessVersion. +func (in *DNSResolverAccessVersion) DeepCopy() *DNSResolverAccessVersion { + if in == nil { + return nil + } + out := new(DNSResolverAccessVersion) + 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 diff --git a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml b/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml deleted file mode 100644 index 3614543d..00000000 --- a/config/crd/bases/dns.networking.miloapis.com_dnscontributiongrants.yaml +++ /dev/null @@ -1,180 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - discovery.miloapis.com/parent-contexts: Project - name: dnscontributiongrants.dns.networking.miloapis.com -spec: - group: dns.networking.miloapis.com - names: - kind: DNSContributionGrant - listKind: DNSContributionGrantList - plural: dnscontributiongrants - singular: dnscontributiongrant - 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: - nameScopes: - items: - type: string - type: array - principal: - properties: - subject: - minLength: 1 - type: string - required: - - subject - type: object - producerID: - type: string - recordTypes: - items: - enum: - - A - - AAAA - - ALIAS - - CNAME - - TXT - - MX - - SRV - - CAA - - NS - - SOA - - PTR - - TLSA - - HTTPS - - SVCB - type: string - type: array - registrationRef: - 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: - - principal - - producerID - - recordTypes - - registrationRef - type: object - x-kubernetes-validations: - - message: registrationRef requires name and UID - rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) - && self.registrationRef.uid.size() > 0 - status: - properties: - activeWriterEpoch: - format: int64 - type: integer - 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 - observedGrantGeneration: - format: int64 - type: integer - observedRegistrationGeneration: - 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_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 0f38938b..2ef3f510 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -38,28 +38,6 @@ spec: type: object spec: properties: - grantRef: - 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 recordSets: items: properties: @@ -326,7 +304,6 @@ spec: - name type: object required: - - grantRef - recordSets - registrationRef type: object @@ -335,12 +312,8 @@ spec: rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0 - - message: grantRef requires name and UID - rule: self.grantRef.name.size() > 0 && has(self.grantRef.uid) && self.grantRef.uid.size() - > 0 - - message: contribution authority references are immutable - rule: self.registrationRef == oldSelf.registrationRef && self.grantRef - == oldSelf.grantRef + - message: contribution registration reference is immutable + rule: self.registrationRef == oldSelf.registrationRef status: properties: conditions: @@ -403,6 +376,7 @@ spec: type: boolean observedGeneration: format: int64 + minimum: 1 type: integer publishedRevision: format: int64 @@ -411,11 +385,17 @@ spec: type: string sequence: format: int64 - type: integer - writerEpoch: - format: int64 + minimum: 1 type: integer type: object + x-kubernetes-validations: + - message: observation sequence must not decrease + rule: '!has(oldSelf.sequence) || (has(self.sequence) && self.sequence + >= oldSelf.sequence)' + - message: eligibility changes require a newer observation sequence + rule: (has(self.eligible) && self.eligible) == (has(oldSelf.eligible) + && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) + || self.sequence > oldSelf.sequence)) required: - spec type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index f743b3d3..45c904b9 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -38,20 +38,6 @@ spec: type: object spec: properties: - authorization: - properties: - sequence: - format: int64 - minimum: 1 - type: integer - writerEpoch: - format: int64 - minimum: 1 - type: integer - required: - - sequence - - writerEpoch - type: object contextRef: description: |- DNSObjectReference pins a namespaced object by API-assigned UID. Generation @@ -107,13 +93,27 @@ spec: minItems: 2 type: array x-kubernetes-list-type: set + version: + properties: + epoch: + format: int64 + minimum: 1 + type: integer + sequence: + format: int64 + minimum: 1 + type: integer + required: + - epoch + - sequence + type: object required: - - authorization - contextRef - port - queryIdentity - region - transports + - version type: object x-kubernetes-validations: - message: contextRef requires name and UID @@ -124,11 +124,10 @@ spec: - message: access identity and transport are immutable rule: self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity && self.port == oldSelf.port && self.transports == oldSelf.transports - - message: writerEpoch is immutable for an access binding - rule: self.authorization.writerEpoch == oldSelf.authorization.writerEpoch - - message: authorization changes require an increasing sequence - rule: self.authorization == oldSelf.authorization || self.authorization.sequence - > oldSelf.authorization.sequence + - message: epoch is immutable for an access binding + rule: self.version.epoch == oldSelf.version.epoch + - message: version changes require an increasing sequence + rule: self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence status: properties: bindingRef: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml index 0adba855..2bdf3da0 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -58,7 +58,7 @@ spec: rule: oldSelf.consumerID == self.consumerID status: properties: - accessWriterEpoch: + accessEpoch: format: int64 type: integer conditions: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b6efcd04..8d7e5599 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,7 +6,6 @@ resources: - bases/dns.networking.miloapis.com_dnszones.yaml - bases/dns.networking.miloapis.com_dnsrecordsets.yaml - bases/dns.networking.miloapis.com_dnszonediscoveries.yaml -- bases/dns.networking.miloapis.com_dnscontributiongrants.yaml - bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml - bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml - bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml diff --git a/config/iam/protected-resources/internal-dns.yaml b/config/iam/protected-resources/internal-dns.yaml index a4803eb8..d39df207 100644 --- a/config/iam/protected-resources/internal-dns.yaml +++ b/config/iam/protected-resources/internal-dns.yaml @@ -54,24 +54,6 @@ spec: --- apiVersion: iam.miloapis.com/v1alpha1 kind: ProtectedResource -metadata: - name: dns.networking.miloapis.com-dnscontributiongrant -spec: - serviceRef: - name: dns.networking.miloapis.com - kind: DNSContributionGrant - plural: dnscontributiongrants - singular: dnscontributiongrant - 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-dnsrecordcontribution spec: diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 987f0eb4..f2f64ee9 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -10,7 +10,6 @@ import ( "encoding/json" "fmt" "net/http" - "reflect" dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" "go.miloapis.com/dns-operator/internal/internaldns/model" @@ -21,6 +20,8 @@ import ( kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) +const subresourceStatus = "status" + const contributionResource = "dnsrecordcontributions" const Path = "/validate-internal-dns" @@ -42,11 +43,11 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm if req.Operation == admissionv1.Delete { return kubeadmission.Allowed("API server authorization applies") } - if req.SubResource == "status" && req.Resource.Resource != contributionResource { + if req.SubResource == subresourceStatus { return kubeadmission.Allowed("API server status authorization applies") } switch req.Resource.Resource { - case "dnsregistrations", contributionResource, "dnscontributiongrants", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": + case "dnsregistrations", contributionResource, "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": // These resources require live references. default: return kubeadmission.Allowed("API server authorization and schema validation apply") @@ -70,8 +71,6 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm err = h.registration(ctx, cl, req) case contributionResource: err = h.contribution(ctx, cl, req) - case "dnscontributiongrants": - err = h.grant(ctx, cl, req) case "dnszoneassociations": err = h.association(ctx, cl, req) case "dnsnamingpolicies": @@ -82,7 +81,7 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm if err != nil { return kubeadmission.Denied(err.Error()) } - return kubeadmission.Allowed("authenticated and scoped DNS writer") + return kubeadmission.Allowed("DNS references are valid") } func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kubeadmission.Request) error { @@ -97,7 +96,7 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if resolverContext.UID != current.Spec.ContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() { return fmt.Errorf("resolver context lifetime is not current") } - if resolverContext.Status.AccessWriterEpoch < 1 || current.Spec.Authorization.WriterEpoch != resolverContext.Status.AccessWriterEpoch { + if resolverContext.Status.AccessEpoch < 1 || current.Spec.Version.Epoch != resolverContext.Status.AccessEpoch { return fmt.Errorf("access writer epoch is retired or not issued") } ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") @@ -108,105 +107,30 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube } func (h *Handler) contribution(ctx context.Context, cl client.Client, req kubeadmission.Request) error { - var c, old dnsv1alpha1.DNSRecordContribution - if err := decode(req, &c); err != nil { + var contribution dnsv1alpha1.DNSRecordContribution + if err := decode(req, &contribution); err != nil { return err } - if len(req.OldObject.Raw) > 0 { - if err := json.Unmarshal(req.OldObject.Raw, &old); err != nil { - return err - } - } - bindEpoch := false - if req.SubResource == "status" { - previous, current := old.Status, c.Status - previous.Conditions, current.Conditions = nil, nil - previous.PublishedRevision, current.PublishedRevision = 0, 0 - if reflect.DeepEqual(previous, current) { - return nil - } - // Binding an issued epoch cannot introduce endpoint eligibility. - if previous.WriterEpoch == 0 && previous.ObservedGeneration == 0 && previous.Sequence == 0 && !previous.Eligible && current.WriterEpoch > 0 { - current.WriterEpoch = 0 - bindEpoch = reflect.DeepEqual(previous, current) - } - } - var g dnsv1alpha1.DNSContributionGrant - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.GrantRef.Name}, &g); err != nil { - return fmt.Errorf("grant lookup: %w", err) - } - if g.UID != c.Spec.GrantRef.UID || !g.DeletionTimestamp.IsZero() { - return fmt.Errorf("grant lifetime is not current") - } - if !bindEpoch && g.Spec.Principal.Subject != req.UserInfo.Username { - return fmt.Errorf("caller does not match the grant's authenticated producer principal") - } - if g.Spec.RegistrationRef.UID != c.Spec.RegistrationRef.UID || g.Spec.RegistrationRef.Name != c.Spec.RegistrationRef.Name { - return fmt.Errorf("grant belongs to another registration") - } - var r dnsv1alpha1.DNSRegistration - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: c.Spec.RegistrationRef.Name}, &r); err != nil { - return fmt.Errorf("registration lookup: %w", err) - } - if r.UID != c.Spec.RegistrationRef.UID || r.Generation != c.Spec.RegistrationRef.Generation || !r.DeletionTimestamp.IsZero() { - return fmt.Errorf("registration lifetime or policy generation is not current") + var registration dnsv1alpha1.DNSRegistration + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: contribution.Spec.RegistrationRef.Name}, ®istration); err != nil { + return err } - if g.Status.ActiveWriterEpoch < 1 || g.Status.ObservedRegistrationGeneration != r.Generation || g.Status.ObservedGrantGeneration != g.Generation { - return fmt.Errorf("grant authority is not issued for the current registration generation") + if registration.UID != contribution.Spec.RegistrationRef.UID || registration.Generation != contribution.Spec.RegistrationRef.Generation || !registration.DeletionTimestamp.IsZero() { + return fmt.Errorf("registration lifetime or generation is not current") } - var zone dnsv1alpha1.DNSZone - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: r.Spec.DNSZoneRef.Name}, &zone); err != nil { - return fmt.Errorf("private zone lookup: %w", err) - } - if zone.Spec.Visibility != dnsv1alpha1.DNSZoneVisibilityPrivate { - return fmt.Errorf("internal contributions require a private zone") + zone, err := currentPrivateZone(ctx, cl, req.Namespace, registration.Spec.DNSZoneRef) + if err != nil { + return err } - for _, rrset := range c.Spec.RecordSets { - for _, record := range rrset.Records { - if err := model.ValidatePrivateRecordOwner(record.Name, zone.Spec.DomainName, string(rrset.RecordType)); err != nil { + for _, set := range contribution.Spec.RecordSets { + if !containsType(registration.Spec.RecordTypes, set.RecordType) { + return fmt.Errorf("record type is outside the registration") + } + for _, record := range set.Records { + if err := model.ValidatePrivateRecordOwner(record.Name, zone.Spec.DomainName, string(set.RecordType)); err != nil { return err } } - if !containsType(g.Spec.RecordTypes, rrset.RecordType) || !containsType(r.Spec.RecordTypes, rrset.RecordType) { - return fmt.Errorf("record type %s is outside the writer's scope", rrset.RecordType) - } - } - if bindEpoch { - if c.Status.WriterEpoch != g.Status.ActiveWriterEpoch { - return fmt.Errorf("writer epoch is retired or not issued") - } - return nil - } - if req.SubResource == "status" { - if !reflect.DeepEqual(old.Status.Conditions, c.Status.Conditions) || old.Status.PublishedRevision != c.Status.PublishedRevision { - return fmt.Errorf("publisher observations cannot change publication results") - } - if c.Status.WriterEpoch != g.Status.ActiveWriterEpoch { - return fmt.Errorf("writer epoch is retired or not issued") - } - if c.Status.Sequence <= old.Status.Sequence { - return fmt.Errorf("producer observation sequence must increase") - } - if c.Status.ObservedGeneration != c.Generation { - return fmt.Errorf("producer observation must match the contribution generation") - } - } else if req.Operation != admissionv1.Delete && !reflect.DeepEqual(old.Status, c.Status) && req.Operation != admissionv1.Create { - return fmt.Errorf("producer observations must use the status subresource") - } - return nil -} - -func (h *Handler) grant(ctx context.Context, cl client.Client, req kubeadmission.Request) error { - var g dnsv1alpha1.DNSContributionGrant - if err := decode(req, &g); err != nil { - return err - } - var r dnsv1alpha1.DNSRegistration - if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: g.Spec.RegistrationRef.Name}, &r); err != nil { - return err - } - if r.UID != g.Spec.RegistrationRef.UID { - return fmt.Errorf("registration lifetime is not current") } return nil } diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 3d36e8f1..60f4f726 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -19,86 +19,14 @@ import ( kubeadmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -func TestContributionAuthenticatedWriter(t *testing.T) { - producer := "system:serviceaccount:compute-system:dns-publisher" - registration := &dnsv1alpha1.DNSRegistration{ - ObjectMeta: metav1.ObjectMeta{Name: "api", Namespace: "project", UID: "reg-uid", Generation: 3}, - Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: "private"}, RecordTypes: []dnsv1alpha1.RRType{"AAAA"}}, - } - grant := &dnsv1alpha1.DNSContributionGrant{ - ObjectMeta: metav1.ObjectMeta{Name: "compute", Namespace: "project", UID: "grant-uid"}, - Spec: dnsv1alpha1.DNSContributionGrantSpec{ - RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid"}, - Principal: dnsv1alpha1.DNSProducerPrincipal{Subject: producer}, - RecordTypes: []dnsv1alpha1.RRType{"AAAA"}, - }, - Status: dnsv1alpha1.DNSContributionGrantStatus{ActiveWriterEpoch: 8, ObservedRegistrationGeneration: 3}, - } - contribution := dnsv1alpha1.DNSRecordContribution{ - ObjectMeta: metav1.ObjectMeta{Name: "api-east", Namespace: "project", UID: "contribution-uid", Generation: 2}, - Spec: dnsv1alpha1.DNSRecordContributionSpec{ - RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: "api", UID: "reg-uid", Generation: 3}, - GrantRef: dnsv1alpha1.DNSObjectReference{Name: "compute", UID: "grant-uid"}, - }, - Status: dnsv1alpha1.DNSRecordContributionStatus{ObservedGeneration: 2, WriterEpoch: 8, Sequence: 18, Eligible: true}, - } - scheme := runtime.NewScheme() - if err := dnsv1alpha1.AddToScheme(scheme); err != nil { - t.Fatal(err) - } - h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(registration, grant, &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "corp.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}}).Build()} - cases := []struct { - name string - user string - mutate func(*dnsv1alpha1.DNSRecordContribution) - allowed bool - }{ - {"scoped fresh producer", producer, nil, true}, - {"another producer", "system:serviceaccount:connector-system:dns-publisher", nil, false}, - {"controller binds issued epoch", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { - c.Status.Conditions = []metav1.Condition{{Type: "AuthorityBound", Status: metav1.ConditionTrue}} - }, true}, - {"controller cannot bind another epoch", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 99 }, false}, - {"epoch binding cannot assert health", "rbac-authorized-controller", func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Eligible = true }, false}, - - {"retired epoch", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.WriterEpoch = 7 }, false}, - {"unchanged observation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 17 }, true}, - {"decreasing sequence", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.Sequence = 16 }, false}, - {"stale generation", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.ObservedGeneration = 1 }, false}, - {"forged grant UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.GrantRef.UID = "other-grant" }, false}, - {"retired registration UID", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Spec.RegistrationRef.UID = "old-reg" }, false}, - {"publication fields", producer, func(c *dnsv1alpha1.DNSRecordContribution) { c.Status.PublishedRevision = 1000 }, false}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - c := contribution.DeepCopy() - old := contribution.DeepCopy() - old.Status.Sequence = 17 - if tc.name == "controller binds issued epoch" || tc.name == "controller cannot bind another epoch" || tc.name == "epoch binding cannot assert health" { - old.Status = dnsv1alpha1.DNSRecordContributionStatus{} - c.Status = dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 8} - } - if tc.mutate != nil { - tc.mutate(c) - } - req := request("dnsrecordcontributions", tc.user, c, old) - req.SubResource = "status" - res := h.Handle(context.Background(), req) - if res.Allowed != tc.allowed { - t.Fatalf("allowed=%v want=%v: %v", res.Allowed, tc.allowed, res.Result) - } - }) - } -} - func TestResourceAccessUsesAPIServerAuthorization(t *testing.T) { h := Handler{} for _, resource := range []string{"dnsresolvercontexts", "dnsresolverbindings", "dnspublicationmanifests", "dnspublicationchunks", "dnstransportoutboxes", "dnspublicationownerships"} { if !h.Handle(context.Background(), request(resource, "authorized-user", map[string]any{}, nil)).Allowed { - t.Fatalf("admission duplicates resource authorization for %s", resource) + t.Fatalf("admission duplicates reaccess version for %s", resource) } } - req := request("dnscontributiongrants", "authorized-user", map[string]any{}, nil) + req := request("dnsrecordcontributions", "authorized-user", map[string]any{}, nil) req.SubResource = "status" if !h.Handle(context.Background(), req).Allowed { t.Fatal("admission duplicates status authorization") @@ -142,11 +70,11 @@ func TestInternalAdmissionPreservesPublicZoneControllerStatus(t *testing.T) { } } -func TestRetiredProductIntentCanBeDeletedWithoutLiveGrant(t *testing.T) { +func TestProductIntentCanBeDeletedWithoutLiveReferences(t *testing.T) { h := Handler{} req := kubeadmission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete, Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: "v1alpha1", Resource: "dnsrecordcontributions"}, UserInfo: authenticationv1.UserInfo{Username: "system:serviceaccount:compute:publisher"}}} if !h.Handle(context.Background(), req).Allowed { - t.Fatal("retired contribution cleanup required a live deleted grant") + t.Fatal("contribution cleanup required live references") } req.Resource.Resource = "dnspublicationownerships" if !h.Handle(context.Background(), req).Allowed { @@ -160,12 +88,12 @@ func TestResolverAccessRequiresCurrentContext(t *testing.T) { if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } - resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessWriterEpoch: 3}} + resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessEpoch: 3}} setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build()} - old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Authorization: dnsv1alpha1.DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 9}}} + old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: dnsv1alpha1.DNSResolverAccessVersion{Epoch: 3, Sequence: 9}}} current := old.DeepCopy() - current.Spec.Authorization.Sequence = 10 + current.Spec.Version.Sequence = 10 req := request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if res := h.Handle(context.Background(), req); !res.Allowed { t.Fatalf("current integration update denied: %v", res.Result) @@ -232,7 +160,7 @@ func TestRegistrationRequiresCurrentZone(t *testing.T) { func TestPublicationStatusDoesNotChangePublisherObservation(t *testing.T) { h := Handler{Client: fake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()} - old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{WriterEpoch: 4, Sequence: 10, Eligible: true}} + old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{Sequence: 10, Eligible: true}} current := old.DeepCopy() current.Status.PublishedRevision = 12 current.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue}} @@ -248,3 +176,23 @@ func TestPublicationStatusDoesNotChangePublisherObservation(t *testing.T) { t.Fatal("publication update changed the producer observation without a grant") } } + +func TestContributionValidatesReferencesForRBACAuthorizedCallers(t *testing.T) { + scheme := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + zone := &dnsv1alpha1.DNSZone{ObjectMeta: metav1.ObjectMeta{Name: "private", Namespace: "project", UID: "zone-uid"}, Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "datum.internal", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}} + registration := &dnsv1alpha1.DNSRegistration{ObjectMeta: metav1.ObjectMeta{Name: "web", Namespace: "project", UID: "registration", Generation: 1}, Spec: dnsv1alpha1.DNSRegistrationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: zone.UID}, Name: "web", RecordTypes: []dnsv1alpha1.RRType{dnsv1alpha1.RRTypeA}}} + handler := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone, registration).Build()} + contribution := &dnsv1alpha1.DNSRecordContribution{Spec: dnsv1alpha1.DNSRecordContributionSpec{RegistrationRef: dnsv1alpha1.DNSObjectReference{Name: registration.Name, UID: registration.UID, Generation: registration.Generation}, RecordSets: []dnsv1alpha1.DNSContributionRecordSet{{RecordType: dnsv1alpha1.RRTypeA, Records: []dnsv1alpha1.RecordEntry{{Name: "web", A: &dnsv1alpha1.ARecordSpec{Content: "10.0.0.8"}}}}}}} + for _, caller := range []string{"compute-publisher", "connect-publisher"} { + if result := handler.Handle(context.Background(), request("dnsrecordcontributions", caller, contribution, nil)); !result.Allowed { + t.Fatalf("valid contribution rejected for %s: %v", caller, result.Result) + } + } + contribution.Spec.RegistrationRef.UID = "replacement" + if result := handler.Handle(context.Background(), request("dnsrecordcontributions", "compute-publisher", contribution, nil)); result.Allowed { + t.Fatal("contribution accepted a mismatched registration lifetime") + } +} diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index 272e7cda..a7257475 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -17,7 +17,7 @@ import ( "github.com/miekg/dns" ) -const APIVersion = "internal.dns.networking.miloapis.com/v1alpha4" +const APIVersion = "internal.dns.networking.miloapis.com/v1alpha5" type Kind string @@ -77,9 +77,9 @@ const ( TransportTCP Transport = "TCP" ) -type Authorization struct { - IssuerEpoch uint64 `json:"issuerEpoch"` - Revision uint64 `json:"revision"` +type AccessVersion struct { + Epoch uint64 `json:"epoch"` + Sequence uint64 `json:"sequence"` } type Backend struct { @@ -109,7 +109,7 @@ type Binding struct { Zones []ZoneAttachment `json:"zones"` NodeBackends []Backend `json:"nodeBackends"` ClusterBackends []Backend `json:"clusterBackends"` - Authorization Authorization `json:"authorization"` + Version AccessVersion `json:"version"` Tombstone bool `json:"tombstone,omitempty"` } @@ -188,8 +188,8 @@ func (s ServingSnapshot) Validate() error { } seenVIP[raw] = b.BindingUID } - if b.Port == 0 || len(b.Transports) == 0 || b.Authorization.IssuerEpoch == 0 || b.Authorization.Revision == 0 { - return fmt.Errorf("binding %q has incomplete transport or authorization", b.BindingUID) + if b.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 err := validateTransports(b.Transports); err != nil { return fmt.Errorf("binding %q: %w", b.BindingUID, err) @@ -318,7 +318,7 @@ func (c PublicationChunk) Validate() error { type RRRecord struct { Content string `json:"content"` ContributionUID string `json:"contributionUID,omitempty"` - WriterEpoch uint64 `json:"writerEpoch,omitempty"` + Generation uint64 `json:"generation,omitempty"` Sequence uint64 `json:"sequence,omitempty"` Eligible bool `json:"eligible"` } @@ -347,8 +347,7 @@ type RegistrationFence struct { // ObservationFence carries the accepted high-water mark, including withdrawn observations. type ObservationFence struct { ContributionUID string `json:"contributionUID"` - GrantUID string `json:"grantUID"` - WriterEpoch uint64 `json:"writerEpoch"` + Generation uint64 `json:"generation"` Sequence uint64 `json:"sequence"` } @@ -373,7 +372,7 @@ func (p PublicationPlan) Validate() error { observationByUID := make(map[string]ObservationFence, len(p.ObservationFences)) previousContribution := "" for _, observation := range p.ObservationFences { - if observation.ContributionUID == "" || observation.GrantUID == "" || observation.WriterEpoch == 0 || observation.Sequence == 0 { + if observation.ContributionUID == "" || observation.Generation == 0 || observation.Sequence == 0 { return errors.New("publication observation fence is incomplete") } if previousContribution != "" && observation.ContributionUID <= previousContribution { @@ -411,12 +410,12 @@ func (p PublicationPlan) Validate() error { return fmt.Errorf("RRset %q contains an invalid record", key) } if rec.ContributionUID == "" { - if rec.WriterEpoch != 0 || rec.Sequence != 0 { + if rec.Generation != 0 || rec.Sequence != 0 { return fmt.Errorf("RRset %q static record contains contribution fence data", key) } } else { observation, ok := observationByUID[rec.ContributionUID] - if !ok || rec.WriterEpoch != observation.WriterEpoch || rec.Sequence != observation.Sequence { + if !ok || rec.Generation != observation.Generation || rec.Sequence != observation.Sequence { return fmt.Errorf("RRset %q record fence does not match contribution observation %q", key, rec.ContributionUID) } } diff --git a/internal/internaldns/model/model_test.go b/internal/internaldns/model/model_test.go index 82b6ad87..470101be 100644 --- a/internal/internaldns/model/model_test.go +++ b/internal/internaldns/model/model_test.go @@ -42,7 +42,7 @@ func TestServingSnapshotRejectsSharedDestination(t *testing.T) { t.Parallel() now := time.Now() b := func(uid, vpc string) Binding { - return Binding{BindingUID: uid, ProjectUID: "project", ContextUID: vpc, BindingGeneration: 1, ConfigurationRevision: 1, ConsumerAddress: "2001:db8::1", ClusterAddress: "2001:db8::2", Port: 53, Transports: []Transport{TransportUDP, TransportTCP}, Authorization: Authorization{IssuerEpoch: 1, Revision: 1}} + return Binding{BindingUID: uid, ProjectUID: "project", ContextUID: vpc, BindingGeneration: 1, ConfigurationRevision: 1, ConsumerAddress: "2001:db8::1", ClusterAddress: "2001:db8::2", Port: 53, Transports: []Transport{TransportUDP, TransportTCP}, Version: AccessVersion{Epoch: 1, Sequence: 1}} } s := ServingSnapshot{Region: "east", Shard: "s1", ConfigurationEpoch: 1, ConfigurationRevision: 1, GeneratedAt: now, Bindings: []Binding{b("one", "vpc-a"), b("two", "vpc-b")}} if err := s.Validate(); err == nil { From 2ea92e4a2946ba5a304e035f117209834acf0fec Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:35:52 -0500 Subject: [PATCH 17/28] refactor: Keep DNS permissions aligned with APIs --- config/iam/roles/dns-viewer.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/iam/roles/dns-viewer.yaml b/config/iam/roles/dns-viewer.yaml index a4549fef..372cb46d 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -29,9 +29,6 @@ spec: - dns.networking.miloapis.com/dnsregistrations.list - dns.networking.miloapis.com/dnsregistrations.get - dns.networking.miloapis.com/dnsregistrations.watch - - dns.networking.miloapis.com/dnscontributiongrants.list - - dns.networking.miloapis.com/dnscontributiongrants.get - - dns.networking.miloapis.com/dnscontributiongrants.watch - dns.networking.miloapis.com/dnsrecordcontributions.list - dns.networking.miloapis.com/dnsrecordcontributions.get - dns.networking.miloapis.com/dnsrecordcontributions.watch From 6065843da719496b2e163ed3256aa9865fae0a60 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:43:19 -0500 Subject: [PATCH 18/28] fix: Keep DNS review branches buildable --- api/v1alpha1/internaldns_validation_envtest_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index fb4eb44e..fdcc64d0 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -164,7 +164,5 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { t.Fatal(err) } }) - validateServiceDNSContracts(t, cl) - validateNamingAPISchemas(t, cl) } From 7fed68cb214457fd9e0148a7a9d47c0c427bafc2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:53:49 -0500 Subject: [PATCH 19/28] fix: Align DNS validation and permission checks --- .internal-dns-e2e/kubernetes/crds.yaml | 4219 +++++++++++++++++ .internal-dns-e2e/kubernetes/image.id | 1 + .internal-dns-e2e/kubernetes/kind-config.yaml | 39 + .../kubernetes/platform-bootstrap.json | 2566 ++++++++++ .../kubernetes/platform-internal.kubeconfig | 19 + .../kubernetes/platform.kubeconfig | 18 + .../kubernetes/project-a-bootstrap.json | 946 ++++ .../kubernetes/project-a-internal.kubeconfig | 19 + .../kubernetes/project-a.kubeconfig | 18 + .../kubernetes/project-b-bootstrap.json | 946 ++++ .../kubernetes/project-b-internal.kubeconfig | 19 + .../kubernetes/project-b.kubeconfig | 18 + .../internaldns/admission/handler_test.go | 19 - 13 files changed, 8828 insertions(+), 19 deletions(-) create mode 100644 .internal-dns-e2e/kubernetes/crds.yaml create mode 100644 .internal-dns-e2e/kubernetes/image.id create mode 100644 .internal-dns-e2e/kubernetes/kind-config.yaml create mode 100644 .internal-dns-e2e/kubernetes/platform-bootstrap.json create mode 100644 .internal-dns-e2e/kubernetes/platform-internal.kubeconfig create mode 100644 .internal-dns-e2e/kubernetes/platform.kubeconfig create mode 100644 .internal-dns-e2e/kubernetes/project-a-bootstrap.json create mode 100644 .internal-dns-e2e/kubernetes/project-a-internal.kubeconfig create mode 100644 .internal-dns-e2e/kubernetes/project-a.kubeconfig create mode 100644 .internal-dns-e2e/kubernetes/project-b-bootstrap.json create mode 100644 .internal-dns-e2e/kubernetes/project-b-internal.kubeconfig create mode 100644 .internal-dns-e2e/kubernetes/project-b.kubeconfig diff --git a/.internal-dns-e2e/kubernetes/crds.yaml b/.internal-dns-e2e/kubernetes/crds.yaml new file mode 100644 index 00000000..00cbe9d7 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/crds.yaml @@ -0,0 +1,4219 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnscontributiongrants.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSContributionGrant + listKind: DNSContributionGrantList + plural: dnscontributiongrants + singular: dnscontributiongrant + 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: + nameScopes: + items: + type: string + type: array + principal: + properties: + subject: + minLength: 1 + type: string + required: + - subject + type: object + producerID: + type: string + recordTypes: + items: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + type: array + registrationRef: + 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: + - principal + - producerID + - recordTypes + - registrationRef + type: object + x-kubernetes-validations: + - message: registrationRef requires name and UID + rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) + && self.registrationRef.uid.size() > 0 + status: + properties: + activeWriterEpoch: + format: int64 + type: integer + 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 + observedGrantGeneration: + format: int64 + type: integer + observedRegistrationGeneration: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsmanagednamespaces.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSManagedNamespace + listKind: DNSManagedNamespaceList + plural: dnsmanagednamespaces + singular: dnsmanagednamespace + 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: |- + DNSManagedNamespace is the DNS-owned lookup and lifecycle record for the + canonical private namespace of a VPC. Product controllers read its status; + only the DNS platform controller writes the object. + properties: + dnsZoneClassName: + type: string + domainSuffix: + type: string + projectUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + vpcRef: + 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: + - dnsZoneClassName + - domainSuffix + - projectUID + - vpcRef + type: object + status: + properties: + associationRef: + 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 + canonicalSuffix: + type: string + 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 + dnsZoneRef: + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsnamingpolicies.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSNamingPolicy + listKind: DNSNamingPolicyList + plural: dnsnamingpolicies + singular: dnsnamingpolicy + 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: + additionalNames: + items: + properties: + dnsZoneRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + namePrefix: + type: string + selector: + description: |- + Selector matches DNSRegistration metadata labels within this DNS context. + An omitted or empty selector matches every registration in the context. + properties: + matchExpressions: + items: + properties: + key: + maxLength: 317 + minLength: 1 + type: string + operator: + description: A label selector operator is the set + of operators that can be used in a selector requirement. + enum: + - In + - NotIn + - Exists + - DoesNotExist + type: string + values: + items: + maxLength: 63 + pattern: ^([A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?)?$ + type: string + maxItems: 64 + type: array + required: + - key + - operator + type: object + x-kubernetes-validations: + - message: selector operator requires compatible values + rule: '(self.operator in [''In'', ''NotIn'']) ? (has(self.values) + && self.values.size() > 0) : (!has(self.values) || + self.values.size() == 0)' + maxItems: 64 + type: array + matchLabels: + additionalProperties: + type: string + maxProperties: 64 + type: object + type: object + required: + - dnsZoneRef + - namePrefix + type: object + x-kubernetes-validations: + - message: dnsZoneRef requires a name + rule: self.dnsZoneRef.name.size() > 0 + maxItems: 256 + type: array + dnsZoneRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + priority: + format: int32 + type: integer + resolverContextRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + scopePrefix: + type: string + vpcRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + required: + - additionalNames + type: object + x-kubernetes-validations: + - message: exactly one named vpcRef or resolverContextRef is required + rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() + > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) + && self.resolverContextRef.name.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 + resolvedAdditionalNames: + items: + properties: + dnsZoneRef: + 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 + namePrefix: + type: string + selector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - dnsZoneRef + - namePrefix + type: object + type: array + resolvedDNSZoneRef: + 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 + resolvedResolverContextRef: + 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 + resolvedVPCRef: + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +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 +--- +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 persists an authenticated high-water sequence even + while its observation is unhealthy or withdrawn. + properties: + epoch: + format: int64 + type: integer + grantUID: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + sequence: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - epoch + - grantUID + - sequence + - uid + type: object + type: array + generatedAt: + format: date-time + type: string + previousRevision: + format: int64 + type: integer + registrationFences: + items: + properties: + generation: + format: int64 + type: integer + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - generation + - uid + type: object + type: array + revision: + format: int64 + 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: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + streamSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +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 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsrecordcontributions.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSRecordContribution + listKind: DNSRecordContributionList + plural: dnsrecordcontributions + singular: dnsrecordcontribution + 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: + grantRef: + 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 + recordSets: + items: + properties: + recordType: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + records: + items: + description: RecordEntry represents one owner name and its + values. + properties: + a: + description: Exactly one of the following type-specific + fields should be set matching RecordType. + properties: + content: + format: ipv4 + type: string + required: + - content + type: object + aaaa: + properties: + content: + format: ipv6 + type: string + required: + - content + type: object + alias: + description: |- + ALIASRecordSpec is like CNAME but intended for providers that implement an ALIAS/ANAME-style record. + The content is a hostname (FQDN or relative) with an optional trailing dot. + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + caa: + properties: + flag: + description: 0–255 flag + maximum: 255 + minimum: 0 + type: integer + tag: + description: 'RFC-style tags: keep it simple: [a-z0-9]+' + minLength: 1 + pattern: ^[a-z0-9]+$ + type: string + value: + minLength: 1 + type: string + required: + - flag + - tag + - value + type: object + cname: + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + https: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + mx: + properties: + exchange: + minLength: 1 + type: string + preference: + maximum: 65535 + minimum: 0 + type: integer + required: + - exchange + - preference + type: object + name: + description: Name is the owner name (relative to the zone + or FQDN). + minLength: 1 + pattern: ^(@|[A-Za-z0-9*._-]+)$ + type: string + ns: + properties: + content: + description: |- + Require a hostname (FQDN or relative), allow optional trailing dot, no underscores. + Labels: 1-63 chars, alphanum with interior hyphens, total length <=253. + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\.?$ + type: string + required: + - content + type: object + ptr: + properties: + content: + type: string + required: + - content + type: object + soa: + properties: + expire: + format: int32 + type: integer + mname: + minLength: 1 + type: string + refresh: + format: int32 + type: integer + retry: + format: int32 + type: integer + rname: + minLength: 1 + type: string + serial: + format: int32 + type: integer + ttl: + format: int32 + type: integer + required: + - mname + - rname + type: object + srv: + properties: + port: + maximum: 65535 + minimum: 0 + type: integer + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + minLength: 1 + type: string + weight: + maximum: 65535 + minimum: 0 + type: integer + required: + - port + - priority + - target + - weight + type: object + svcb: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + tlsa: + properties: + certData: + type: string + matchingType: + type: integer + selector: + type: integer + usage: + type: integer + required: + - certData + - matchingType + - selector + - usage + type: object + ttl: + description: TTL optionally overrides TTL for this owner/RRset. + format: int64 + type: integer + txt: + properties: + content: + type: string + required: + - content + type: object + required: + - name + type: object + type: array + required: + - recordType + - records + type: object + type: array + registrationRef: + 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: + - grantRef + - recordSets + - registrationRef + type: object + x-kubernetes-validations: + - message: registrationRef requires name, UID, and generation + rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) + && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) + && self.registrationRef.generation > 0 + - message: grantRef requires name and UID + rule: self.grantRef.name.size() > 0 && has(self.grantRef.uid) && self.grantRef.uid.size() + > 0 + - message: contribution authority references are immutable + rule: self.registrationRef == oldSelf.registrationRef && self.grantRef + == oldSelf.grantRef + 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 + eligible: + type: boolean + observedGeneration: + format: int64 + type: integer + publishedRevision: + format: int64 + type: integer + reason: + type: string + sequence: + format: int64 + type: integer + writerEpoch: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsrecordsets.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSRecordSet + listKind: DNSRecordSetList + plural: dnsrecordsets + singular: dnsrecordset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSRecordSet is the Schema for the dnsrecordsets API + 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: spec defines the desired state of DNSRecordSet + properties: + dnsZoneRef: + description: DNSZoneRef references the DNSZone (same namespace) this + recordset belongs to. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: dnsZoneRef.name must be set + rule: self.name != '' + recordType: + description: RecordType is the DNS RR type for this recordset. + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + records: + description: Records contains one or more owner names with values + appropriate for the RecordType. + items: + description: RecordEntry represents one owner name and its values. + properties: + a: + description: Exactly one of the following type-specific fields + should be set matching RecordType. + properties: + content: + format: ipv4 + type: string + required: + - content + type: object + aaaa: + properties: + content: + format: ipv6 + type: string + required: + - content + type: object + alias: + description: |- + ALIASRecordSpec is like CNAME but intended for providers that implement an ALIAS/ANAME-style record. + The content is a hostname (FQDN or relative) with an optional trailing dot. + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + caa: + properties: + flag: + description: 0–255 flag + maximum: 255 + minimum: 0 + type: integer + tag: + description: 'RFC-style tags: keep it simple: [a-z0-9]+' + minLength: 1 + pattern: ^[a-z0-9]+$ + type: string + value: + minLength: 1 + type: string + required: + - flag + - tag + - value + type: object + cname: + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + https: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + mx: + properties: + exchange: + minLength: 1 + type: string + preference: + maximum: 65535 + minimum: 0 + type: integer + required: + - exchange + - preference + type: object + name: + description: Name is the owner name (relative to the zone or + FQDN). + minLength: 1 + pattern: ^(@|[A-Za-z0-9*._-]+)$ + type: string + ns: + properties: + content: + description: |- + Require a hostname (FQDN or relative), allow optional trailing dot, no underscores. + Labels: 1-63 chars, alphanum with interior hyphens, total length <=253. + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\.?$ + type: string + required: + - content + type: object + ptr: + properties: + content: + type: string + required: + - content + type: object + soa: + properties: + expire: + format: int32 + type: integer + mname: + minLength: 1 + type: string + refresh: + format: int32 + type: integer + retry: + format: int32 + type: integer + rname: + minLength: 1 + type: string + serial: + format: int32 + type: integer + ttl: + format: int32 + type: integer + required: + - mname + - rname + type: object + srv: + properties: + port: + maximum: 65535 + minimum: 0 + type: integer + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + minLength: 1 + type: string + weight: + maximum: 65535 + minimum: 0 + type: integer + required: + - port + - priority + - target + - weight + type: object + svcb: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + tlsa: + properties: + certData: + type: string + matchingType: + type: integer + selector: + type: integer + usage: + type: integer + required: + - certData + - matchingType + - selector + - usage + type: object + ttl: + description: TTL optionally overrides TTL for this owner/RRset. + format: int64 + type: integer + txt: + properties: + content: + type: string + required: + - content + type: object + required: + - name + type: object + minItems: 1 + type: array + required: + - dnsZoneRef + - recordType + - records + type: object + status: + description: status defines the observed state of DNSRecordSet + properties: + conditions: + description: Conditions includes Accepted and Programmed readiness. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + recordSets: + description: RecordSets captures per-owner (per name) status and conditions. + items: + properties: + conditions: + description: Conditions captures per-name readiness information + such as RecordProgrammed. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the owner name this status pertains to. + type: string + required: + - name + type: object + type: array + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.dnsZoneRef.name + - jsonPath: .spec.recordType + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsregistrations.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSRegistration + listKind: DNSRegistrationList + plural: dnsregistrations + singular: dnsregistration + 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: + dnsZoneRef: + 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 + name: + type: string + publicationPolicy: + enum: + - EligibleContributions + - Persistent + type: string + recordTypes: + items: + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + type: array + reservedDescendants: + items: + type: string + type: array + ttlSeconds: + description: TTLSeconds bounds cached dynamic answers to at most 30 + seconds after withdrawal. + format: int32 + maximum: 30 + minimum: 1 + type: integer + required: + - dnsZoneRef + - name + - publicationPolicy + - recordTypes + - ttlSeconds + type: object + x-kubernetes-validations: + - message: dnsZoneRef must pin the zone UID + rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 + status: + properties: + canonicalFQDN: + type: string + 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 + observedGeneration: + format: int64 + type: integer + publicationRevision: + format: int64 + type: integer + publicationWriterEpoch: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsresolveraccessbindings.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverAccessBinding + listKind: DNSResolverAccessBindingList + plural: dnsresolveraccessbindings + singular: dnsresolveraccessbinding + 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: + authorization: + properties: + sequence: + format: int64 + minimum: 1 + type: integer + writerEpoch: + format: int64 + minimum: 1 + type: integer + required: + - sequence + - writerEpoch + type: object + contextRef: + 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 + port: + enum: + - 53 + format: int32 + type: integer + queryIdentity: + properties: + type: + enum: + - DestinationAddress + type: string + value: + maxLength: 253 + minLength: 1 + type: string + required: + - type + - value + type: object + region: + maxLength: 253 + minLength: 1 + type: string + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 2 + type: array + x-kubernetes-list-type: set + required: + - authorization + - contextRef + - port + - queryIdentity + - region + - transports + type: object + x-kubernetes-validations: + - message: contextRef requires name and UID + rule: self.contextRef.name.size() > 0 && has(self.contextRef.uid) && + self.contextRef.uid.size() > 0 + - message: contextRef is immutable + rule: self.contextRef == oldSelf.contextRef + - message: access identity and transport are immutable + rule: self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity + && self.port == oldSelf.port && self.transports == oldSelf.transports + - message: writerEpoch is immutable for an access binding + rule: self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + - message: authorization changes require an increasing sequence + rule: self.authorization == oldSelf.authorization || self.authorization.sequence + > oldSelf.authorization.sequence + status: + properties: + bindingRef: + 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 + 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 + observedSequence: + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: dnsresolverbindings.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverBinding + listKind: DNSResolverBindingList + plural: dnsresolverbindings + singular: dnsresolverbinding + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DNSResolverBinding is the DNS-service projection of a consumer + context and access binding. + properties: + authorization: + properties: + sequence: + format: int64 + minimum: 1 + type: integer + writerEpoch: + format: int64 + minimum: 1 + type: integer + required: + - sequence + - writerEpoch + type: object + configuration: + properties: + generation: + format: int64 + minimum: 1 + type: integer + listeners: + properties: + node: + properties: + address: + maxLength: 253 + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + regional: + properties: + address: + maxLength: 253 + minLength: 1 + type: string + port: + enum: + - 53 + format: int32 + type: integer + transports: + items: + enum: + - UDP + - TCP + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - address + - port + - transports + type: object + required: + - node + - regional + type: object + x-kubernetes-validations: + - message: listener identity is immutable for an access binding + rule: oldSelf == self + revision: + format: int64 + minimum: 1 + type: integer + zoneRefs: + items: + description: |- + DNSObjectReference pins a namespaced object by API-assigned UID. Generation + is used where a writer must also be fenced against policy changes. + properties: + generation: + format: int64 + 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 + - listeners + - revision + - zoneRefs + type: object + x-kubernetes-validations: + - message: zone references must pin name and UID + rule: self.zoneRefs.all(z, z.name.size() > 0 && has(z.uid) && z.uid.size() + > 0) + placement: + properties: + region: + 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 + required: + - authorization + - configuration + - placement + - source + type: object + x-kubernetes-validations: + - message: configuration fences must not decrease + rule: self.configuration.generation >= oldSelf.configuration.generation + && self.configuration.revision >= oldSelf.configuration.revision + - message: authorization fences must not decrease + rule: self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence >= oldSelf.authorization.sequence) + - message: zone membership changes require a new cache generation and + configuration revision + rule: self.configuration.zoneRefs == oldSelf.configuration.zoneRefs + || (self.configuration.generation > oldSelf.configuration.generation + && self.configuration.revision > oldSelf.configuration.revision) + - message: changing withdrawal state requires a new configuration revision + rule: (has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) + && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision + - message: reactivating withdrawn access requires a new source authorization + fence + rule: '!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) + && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch + || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch + && self.authorization.sequence > oldSelf.authorization.sequence)' + status: + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + memberAcknowledgements: + items: + properties: + authorizationIssuerEpoch: + format: int64 + type: integer + authorizationRevision: + format: int64 + type: integer + error: + type: string + memberID: + type: string + observedAt: + format: date-time + type: string + observedUnixNano: + format: int64 + type: integer + phase: + type: string + revision: + format: int64 + type: integer + snapshotRevision: + format: int64 + type: integer + validUntil: + format: date-time + type: string + writerEpoch: + format: int64 + type: integer + required: + - memberID + - observedAt + - phase + - revision + type: object + type: array + observedConfigurationRevision: + format: int64 + type: integer + phase: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnsresolvercontexts.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSResolverContext + listKind: DNSResolverContextList + plural: dnsresolvercontexts + singular: dnsresolvercontext + 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: |- + DNSResolverContext is the DNS-owned serving and namespace boundary created by + a trusted network integration. consumerID is opaque to DNS and immutable. + properties: + consumerID: + maxLength: 512 + minLength: 1 + type: string + managedNamespace: + properties: + domainSuffix: + description: DomainSuffix is the zone apex requested by the trusted + consumer integration. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$ + type: string + enabled: + type: boolean + type: object + x-kubernetes-validations: + - message: domainSuffix is required when the managed namespace is + enabled + rule: '!has(self.enabled) || !self.enabled || (has(self.domainSuffix) + && self.domainSuffix.size() > 0)' + required: + - consumerID + type: object + x-kubernetes-validations: + - message: consumerID is immutable + rule: oldSelf.consumerID == self.consumerID + - message: managed namespace domainSuffix is immutable + rule: '!has(oldSelf.managedNamespace) || !has(oldSelf.managedNamespace.domainSuffix) + || (has(self.managedNamespace) && has(self.managedNamespace.domainSuffix) + && self.managedNamespace.domainSuffix == oldSelf.managedNamespace.domainSuffix)' + status: + properties: + accessWriterEpoch: + format: int64 + type: integer + 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 + managedNamespace: + properties: + dnsZoneRef: + 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 + suffix: + type: string + type: object + servingTargets: + items: + properties: + region: + type: string + shard: + type: string + required: + - region + - shard + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +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: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnszoneassociations.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZoneAssociation + listKind: DNSZoneAssociationList + plural: dnszoneassociations + singular: dnszoneassociation + 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: + dnsZoneRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + managed: + type: boolean + resolverContextRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + vpcRef: + description: |- + DNSNameReference selects a resource in the same project namespace. + DNS records its resolved lifetime in controller-owned status. + properties: + name: + maxLength: 253 + type: string + required: + - name + type: object + required: + - dnsZoneRef + type: object + x-kubernetes-validations: + - message: exactly one named vpcRef or resolverContextRef is required + rule: (has(self.vpcRef) && has(self.vpcRef.name) && self.vpcRef.name.size() + > 0) != (has(self.resolverContextRef) && has(self.resolverContextRef.name) + && self.resolverContextRef.name.size() > 0) + - message: dnsZoneRef requires a name + rule: self.dnsZoneRef.name.size() > 0 + status: + properties: + bindingRef: + 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 + 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 + resolvedDNSZoneRef: + 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 + resolvedResolverContextRef: + 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 + resolvedVPCRef: + 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 + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnszoneclasses.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZoneClass + listKind: DNSZoneClassList + plural: dnszoneclasses + singular: dnszoneclass + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSZoneClass is the Schema for the dnszoneclasses API + 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: spec defines the desired state of DNSZoneClass + properties: + controllerName: + description: |- + ControllerName names the service that provisions and serves zones of this + class (for example, "powerdns"). + type: string + defaults: + description: Defaults provides optional default values applied to + managed zones. + properties: + defaultTTL: + description: DefaultTTL is the default TTL applied to records + when not otherwise specified. + format: int64 + type: integer + type: object + nameServerPolicy: + description: NameServerPolicy defines how nameservers are assigned + for zones using this class. + properties: + mode: + description: Mode defines which policy to use. + enum: + - Static + type: string + static: + description: Static contains a static list of authoritative nameservers + when Mode == "Static". + properties: + servers: + items: + type: string + type: array + required: + - servers + type: object + required: + - mode + type: object + required: + - controllerName + type: object + status: + description: status defines the observed state of DNSZoneClass + properties: + conditions: + description: |- + Conditions represent the current state of the resource. Common types include + "Accepted" and "Programmed" to standardize readiness reporting across controllers. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnszonediscoveries.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZoneDiscovery + listKind: DNSZoneDiscoveryList + plural: dnszonediscoveries + shortNames: + - dnszd + singular: dnszonediscovery + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Discovered")].status + name: Discovered + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSZoneDiscovery is the Schema for the DNSZone discovery API. + 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: spec defines the desired target for discovery. + properties: + dnsZoneRef: + description: DNSZoneRef references the DNSZone (same namespace) this + discovery targets. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - dnsZoneRef + type: object + status: + description: status contains the discovered data (write-once). + properties: + conditions: + description: Conditions includes Accepted and Discovered. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + recordSets: + description: RecordSets is the set of discovered RRsets grouped by + RecordType. + items: + description: DiscoveredRecordSet groups discovered records by type. + properties: + recordType: + description: RecordType is the DNS RR type for this recordset. + enum: + - A + - AAAA + - ALIAS + - CNAME + - TXT + - MX + - SRV + - CAA + - NS + - SOA + - PTR + - TLSA + - HTTPS + - SVCB + type: string + records: + description: |- + Records contains one or more owner names with values appropriate for the RecordType. + The RecordEntry schema is shared with DNSRecordSet for easy translation. + items: + description: RecordEntry represents one owner name and its + values. + properties: + a: + description: Exactly one of the following type-specific + fields should be set matching RecordType. + properties: + content: + format: ipv4 + type: string + required: + - content + type: object + aaaa: + properties: + content: + format: ipv6 + type: string + required: + - content + type: object + alias: + description: |- + ALIASRecordSpec is like CNAME but intended for providers that implement an ALIAS/ANAME-style record. + The content is a hostname (FQDN or relative) with an optional trailing dot. + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + caa: + properties: + flag: + description: 0–255 flag + maximum: 255 + minimum: 0 + type: integer + tag: + description: 'RFC-style tags: keep it simple: [a-z0-9]+' + minLength: 1 + pattern: ^[a-z0-9]+$ + type: string + value: + minLength: 1 + type: string + required: + - flag + - tag + - value + type: object + cname: + properties: + content: + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?)(?:\.([A-Za-z0-9_](?:[-A-Za-z0-9_]{0,61}[A-Za-z0-9_])?))*\.?$ + type: string + required: + - content + type: object + https: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + mx: + properties: + exchange: + minLength: 1 + type: string + preference: + maximum: 65535 + minimum: 0 + type: integer + required: + - exchange + - preference + type: object + name: + description: Name is the owner name (relative to the zone + or FQDN). + minLength: 1 + pattern: ^(@|[A-Za-z0-9*._-]+)$ + type: string + ns: + properties: + content: + description: |- + Require a hostname (FQDN or relative), allow optional trailing dot, no underscores. + Labels: 1-63 chars, alphanum with interior hyphens, total length <=253. + maxLength: 253 + minLength: 1 + pattern: ^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*\.?$ + type: string + required: + - content + type: object + ptr: + properties: + content: + type: string + required: + - content + type: object + soa: + properties: + expire: + format: int32 + type: integer + mname: + minLength: 1 + type: string + refresh: + format: int32 + type: integer + retry: + format: int32 + type: integer + rname: + minLength: 1 + type: string + serial: + format: int32 + type: integer + ttl: + format: int32 + type: integer + required: + - mname + - rname + type: object + srv: + properties: + port: + maximum: 65535 + minimum: 0 + type: integer + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + minLength: 1 + type: string + weight: + maximum: 65535 + minimum: 0 + type: integer + required: + - port + - priority + - target + - weight + type: object + svcb: + properties: + params: + additionalProperties: + type: string + type: object + priority: + maximum: 65535 + minimum: 0 + type: integer + target: + type: string + required: + - priority + - target + type: object + tlsa: + properties: + certData: + type: string + matchingType: + type: integer + selector: + type: integer + usage: + type: integer + required: + - certData + - matchingType + - selector + - usage + type: object + ttl: + description: TTL optionally overrides TTL for this owner/RRset. + format: int64 + type: integer + txt: + properties: + content: + type: string + required: + - content + type: object + required: + - name + type: object + type: array + required: + - recordType + - records + type: object + type: array + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.dnsZoneRef.name + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project + name: dnszones.dns.networking.miloapis.com +spec: + group: dns.networking.miloapis.com + names: + kind: DNSZone + listKind: DNSZoneList + plural: dnszones + singular: dnszone + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .status.recordCount + name: Records + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSZone is the Schema for the dnszones API + 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: spec defines the desired state of DNSZone + properties: + dnsZoneClassName: + description: DNSZoneClassName references the DNSZoneClass used to + provision this zone. + type: string + domainName: + description: DomainName is the FQDN of the zone (e.g., "example.com"). + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + x-kubernetes-validations: + - message: A domain name is immutable and cannot be changed after + creation + rule: oldSelf == '' || self == oldSelf + - message: Must have at least two segments separated by dots + rule: self.indexOf('.') != -1 + visibility: + default: Public + description: |- + Visibility selects public publication or private resolver context serving. + Existing zones default to Public. Changing visibility requires a new zone. + enum: + - Public + - Private + type: string + x-kubernetes-validations: + - message: visibility is immutable + rule: oldSelf == self + required: + - dnsZoneClassName + - domainName + type: object + status: + description: status defines the observed state of DNSZone + properties: + conditions: + description: Conditions tracks state such as Accepted and Programmed + readiness. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + domainRef: + description: DomainRef references the Domain this zone belongs to. + properties: + name: + type: string + status: + properties: + nameservers: + items: + properties: + hostname: + type: string + ips: + items: + description: NameserverIP captures per-address provenance + for a nameserver. + properties: + address: + type: string + registrantName: + type: string + required: + - address + type: object + type: array + required: + - hostname + type: object + type: array + type: object + required: + - name + type: object + nameservers: + description: Nameservers lists the active authoritative nameservers + for this zone. + items: + type: string + type: array + recordCount: + description: RecordCount is the total number of DNS records in this + zone. + type: integer + type: object + required: + - spec + type: object + selectableFields: + - jsonPath: .spec.domainName + - jsonPath: .status.domainRef.name + served: true + storage: true + subresources: + status: {} diff --git a/.internal-dns-e2e/kubernetes/image.id b/.internal-dns-e2e/kubernetes/image.id new file mode 100644 index 00000000..a3d10dcc --- /dev/null +++ b/.internal-dns-e2e/kubernetes/image.id @@ -0,0 +1 @@ +sha256:5fc2d91b586be5a4bf3a19e695a002a67b2cda673fe992fc25ab6a402c420db8 \ No newline at end of file diff --git a/.internal-dns-e2e/kubernetes/kind-config.yaml b/.internal-dns-e2e/kubernetes/kind-config.yaml new file mode 100644 index 00000000..7ce1ca91 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/kind-config.yaml @@ -0,0 +1,39 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + # No extraPortMappings: NodePorts for Envoy Gateway/Grafana are allocated + # dynamically (see components/envoy-gateway-operator and + # components/observability), so multiple clusters can run concurrently on + # the same Docker host without colliding on fixed host ports. Reach + # services via `kubectl port-forward` or the kind Docker network. + + # Add audit log configurations + kubeadmConfigPatches: + - | + kind: ClusterConfiguration + apiServer: + extraArgs: + audit-policy-file: /etc/kubernetes/policies/audit-policy.yaml + audit-log-path: /var/log/kubernetes/kube-apiserver-audit.log + extraVolumes: + - name: audit-policies + hostPath: /etc/kubernetes/policies + mountPath: /etc/kubernetes/policies + readOnly: true + pathType: "DirectoryOrCreate" + - name: "audit-logs" + hostPath: "/var/log/kubernetes" + mountPath: "/var/log/kubernetes" + readOnly: false + pathType: DirectoryOrCreate + # mount the local file on the control plane + extraMounts: + # The filepath is dynamically set via REPO_DIR environment variable by the Taskfile. + # When running in test-infra repo: ./cluster/audit-policy.yaml + # When running from external repo: ./.test-infra/cluster/audit-policy.yaml + - hostPath: /tmp/datum-internal-dns-audit/audit-policy.yaml + containerPath: /etc/kubernetes/policies/audit-policy.yaml + readOnly: true +networking: + ipFamily: dual diff --git a/.internal-dns-e2e/kubernetes/platform-bootstrap.json b/.internal-dns-e2e/kubernetes/platform-bootstrap.json new file mode 100644 index 00000000..6b185976 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/platform-bootstrap.json @@ -0,0 +1,2566 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "project-e2e" + } + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "automountServiceAccountToken": false, + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-fleet", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-vpc-integration", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status", + "dnsmanagednamespaces", + "dnsmanagednamespaces/status" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-product-writer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations", + "dnsrecordcontributions" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants", + "dnsresolvercontexts", + "dnsmanagednamespaces", + "dnsnamingpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsrecordcontributions/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-customer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsnamingpolicies", + "dnsregistrations" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-grant-issuer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "internal-dns-control-plane" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsregistrations", + "dnscontributiongrants", + "dnsrecordcontributions", + "dnsnamingpolicies", + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnszoneassociations" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings", + "dnspublicationmanifests", + "dnspublicationchunks", + "dnstransportoutboxes", + "dnspublicationownerships", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings/status", + "dnspublicationmanifests/status", + "dnstransportoutboxes/status", + "dnspublicationownerships/status", + "dnsmanagednamespaces/status", + "dnszones/status", + "dnszoneassociations/status", + "dnsnamingpolicies/status", + "dnsregistrations/status", + "dnscontributiongrants/status", + "dnsrecordcontributions/status", + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "internal-dns-control-plane" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "internal-dns-control-plane" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Issuer", + "metadata": { + "name": "internal-dns-selfsigned", + "namespace": "internal-dns-system" + }, + "spec": { + "selfSigned": {} + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "internal-dns-ca", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "internal-dns-ca", + "duration": "24h", + "isCA": true, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-selfsigned" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "internal-dns-ca", + "usages": [ + "cert sign", + "crl sign" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Issuer", + "metadata": { + "name": "internal-dns-ca", + "namespace": "internal-dns-system" + }, + "spec": { + "ca": { + "secretName": "internal-dns-ca" + } + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "internal-dns-nats-server", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "internal-dns-nats-server", + "dnsNames": [ + "nats.internal-dns-system.svc", + "nats.internal-dns-system.svc.cluster.local", + "*.internal-dns-nats.internal-dns-system.svc.cluster.local" + ], + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "internal-dns-nats-server", + "usages": [ + "server auth", + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "internal-dns-admission-tls", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "internal-dns-admission-tls", + "dnsNames": [ + "internal-dns-admission.internal-dns-system.svc", + "dns-control-control-plane" + ], + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "internal-dns-admission-tls", + "usages": [ + "server auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "control-a-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "control-a-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "control-a-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "control-b-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "control-b-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "control-b-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "node-us-central1-0-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "node-us-central1-0-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "node-us-central1-0-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "regional-front-us-central1-0-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "regional-front-us-central1-0-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "regional-front-us-central1-0-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "regional-bind-us-central1-0-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "regional-bind-us-central1-0-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "regional-bind-us-central1-0-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": "regional-bind-us-central1-1-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "commonName": "regional-bind-us-central1-1-nats", + "duration": "24h", + "isCA": false, + "issuerRef": { + "kind": "Issuer", + "name": "internal-dns-ca" + }, + "privateKey": { + "algorithm": "ECDSA", + "size": 256 + }, + "renewBefore": "8h", + "secretName": "regional-bind-us-central1-1-nats", + "usages": [ + "client auth" + ] + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "control-a-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "KQblducnQfd+g76kjTIadcFTVFZIgPuv734QYZzXSlQ=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "control-b-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "B00u+Tf4PT4DpamC8BNds2lpdxTz+IrGAfQybcxMSQU=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "node-us-central1-0-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "j3l6igdJLwzvbMgJHRg/Z3VpA9/OUO+MqRSOOHcSwJU=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "regional-front-us-central1-0-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "d917qR9zgyYuGWEdPxf8Q3DAk9NQKJEC0n+Wy2UvBOo=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "regional-bind-us-central1-0-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "pWd0nJIhjwXyT7awFL7kEb9HL4+cndYZGawrkiNiI0w=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "regional-bind-us-central1-1-password", + "namespace": "internal-dns-system" + }, + "stringData": { + "password": "Oc++tn+qOJ3lICo8IPqo4rpm0AUb1yIbwy2RFWt/fqg=" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "internal-dns-nats-config", + "namespace": "internal-dns-system" + }, + "stringData": { + "nats.conf": "{\"authorization\":{\"users\":[{\"password\":\"KQblducnQfd+g76kjTIadcFTVFZIgPuv734QYZzXSlQ=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.serving.\u003e\",\"dns.private.records.\u003e\",\"$KV.DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.\u003e\",\"$JS.ACK.DNS_PRIVATE.control-plane-us-central1-shared-0-acks.\u003e\",\"_INBOX.\u003e\"]},\"subscribe\":{\"allow\":[\"dns.private.acks.\u003e\",\"_INBOX.\u003e\"]}},\"user\":\"control-a\"},{\"password\":\"B00u+Tf4PT4DpamC8BNds2lpdxTz+IrGAfQybcxMSQU=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.serving.\u003e\",\"dns.private.records.\u003e\",\"$KV.DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.\u003e\",\"$JS.ACK.DNS_PRIVATE.control-plane-us-central1-shared-0-acks.\u003e\",\"_INBOX.\u003e\"]},\"subscribe\":{\"allow\":[\"dns.private.acks.\u003e\",\"_INBOX.\u003e\"]}},\"user\":\"control-b\"},{\"password\":\"j3l6igdJLwzvbMgJHRg/Z3VpA9/OUO+MqRSOOHcSwJU=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.acks.us-central1.shared-0.0195a8650ce9d5c927c91495\",\"$JS.API.STREAM.INFO.DNS_PRIVATE\",\"$JS.API.DIRECT.GET.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.STREAM.INFO.KV_DNS_PRIVATE_SNAPSHOTS\",\"$JS.API.CONSUMER.CREATE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.INFO.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.DELETE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"_INBOX.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.node-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.node-us-central1-0-serving\",\"$JS.ACK.DNS_PRIVATE.node-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.node-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.node-us-central1-0-records\",\"$JS.ACK.DNS_PRIVATE.node-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.node-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.node-us-central1-0-acks.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.node-us-central1-0-acks\",\"$JS.ACK.DNS_PRIVATE.node-us-central1-0-acks.\u003e\"]},\"subscribe\":{\"allow\":[\"_INBOX.\u003e\"]}},\"user\":\"node-us-central1-0\"},{\"password\":\"d917qR9zgyYuGWEdPxf8Q3DAk9NQKJEC0n+Wy2UvBOo=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.acks.us-central1.shared-0.92106c62061ed0349bc66280\",\"$JS.API.STREAM.INFO.DNS_PRIVATE\",\"$JS.API.DIRECT.GET.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.STREAM.INFO.KV_DNS_PRIVATE_SNAPSHOTS\",\"$JS.API.CONSUMER.CREATE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.INFO.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.DELETE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"_INBOX.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-front-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-front-us-central1-0-serving\",\"$JS.ACK.DNS_PRIVATE.regional-front-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-front-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-front-us-central1-0-records\",\"$JS.ACK.DNS_PRIVATE.regional-front-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-front-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-front-us-central1-0-acks.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-front-us-central1-0-acks\",\"$JS.ACK.DNS_PRIVATE.regional-front-us-central1-0-acks.\u003e\"]},\"subscribe\":{\"allow\":[\"_INBOX.\u003e\"]}},\"user\":\"regional-front-us-central1-0\"},{\"password\":\"pWd0nJIhjwXyT7awFL7kEb9HL4+cndYZGawrkiNiI0w=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.acks.us-central1.shared-0.b75afe155467f45fdab6d0a1\",\"$JS.API.STREAM.INFO.DNS_PRIVATE\",\"$JS.API.DIRECT.GET.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.STREAM.INFO.KV_DNS_PRIVATE_SNAPSHOTS\",\"$JS.API.CONSUMER.CREATE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.INFO.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.DELETE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"_INBOX.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-0-serving\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-0-serving.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-0-records\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-0-records.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-0-acks.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-0-acks\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-0-acks.\u003e\"]},\"subscribe\":{\"allow\":[\"_INBOX.\u003e\"]}},\"user\":\"regional-bind-us-central1-0\"},{\"password\":\"Oc++tn+qOJ3lICo8IPqo4rpm0AUb1yIbwy2RFWt/fqg=\",\"permissions\":{\"publish\":{\"allow\":[\"dns.private.acks.us-central1.shared-0.2600654ae9645c6e4439e8ac\",\"$JS.API.STREAM.INFO.DNS_PRIVATE\",\"$JS.API.DIRECT.GET.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.STREAM.INFO.KV_DNS_PRIVATE_SNAPSHOTS\",\"$JS.API.CONSUMER.CREATE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.INFO.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"$JS.API.CONSUMER.DELETE.KV_DNS_PRIVATE_SNAPSHOTS.\u003e\",\"_INBOX.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-1-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-serving\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-serving.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-1-serving\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-1-serving.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-1-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-records\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-records.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-1-records\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-1-records.\u003e\",\"$JS.API.CONSUMER.INFO.DNS_PRIVATE.regional-bind-us-central1-1-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-acks\",\"$JS.API.CONSUMER.CREATE.DNS_PRIVATE.regional-bind-us-central1-1-acks.\u003e\",\"$JS.API.CONSUMER.MSG.NEXT.DNS_PRIVATE.regional-bind-us-central1-1-acks\",\"$JS.ACK.DNS_PRIVATE.regional-bind-us-central1-1-acks.\u003e\"]},\"subscribe\":{\"allow\":[\"_INBOX.\u003e\"]}},\"user\":\"regional-bind-us-central1-1\"}]},\"cluster\":{\"name\":\"internal-dns-e2e\",\"port\":6222,\"routes\":[\"nats-route://internal-dns-nats-0.internal-dns-nats.internal-dns-system.svc.cluster.local:6222\",\"nats-route://internal-dns-nats-1.internal-dns-nats.internal-dns-system.svc.cluster.local:6222\",\"nats-route://internal-dns-nats-2.internal-dns-nats.internal-dns-system.svc.cluster.local:6222\"],\"tls\":{\"ca_file\":\"/tls/ca.crt\",\"cert_file\":\"/tls/tls.crt\",\"key_file\":\"/tls/tls.key\",\"verify\":true}},\"http_port\":8222,\"jetstream\":{\"store_dir\":\"/data\"},\"port\":4222,\"tls\":{\"ca_file\":\"/tls/ca.crt\",\"cert_file\":\"/tls/tls.crt\",\"key_file\":\"/tls/tls.key\",\"verify\":true}}\n" + } + }, + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "internal-dns-rndc", + "namespace": "internal-dns-system" + }, + "stringData": { + "rndc.key": "key \"internal-dns\" { algorithm hmac-sha256; secret \"JAJrowpw8ZbYLQsbOJwdRqM0oGAkRZfGvWS18iWMgOw=\"; };\n" + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "nats", + "namespace": "internal-dns-system" + }, + "spec": { + "ports": [ + { + "name": "client", + "port": 4222 + }, + { + "name": "cluster", + "port": 6222 + } + ], + "selector": { + "app": "internal-dns-nats" + } + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "internal-dns-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "clusterIP": "None", + "ports": [ + { + "name": "client", + "port": 4222 + }, + { + "name": "cluster", + "port": 6222 + } + ], + "publishNotReadyAddresses": true, + "selector": { + "app": "internal-dns-nats" + } + } + }, + { + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": { + "name": "internal-dns-nats", + "namespace": "internal-dns-system" + }, + "spec": { + "podManagementPolicy": "Parallel", + "replicas": 3, + "selector": { + "matchLabels": { + "app": "internal-dns-nats" + } + }, + "serviceName": "internal-dns-nats", + "template": { + "metadata": { + "labels": { + "app": "internal-dns-nats" + } + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "args": [ + "-c", + "/config/nats.conf", + "-n", + "$(POD_NAME)" + ], + "command": [ + "nats-server" + ], + "env": [ + { + "name": "POD_NAME", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.name" + } + } + } + ], + "image": "nats@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00", + "name": "nats", + "readinessProbe": { + "httpGet": { + "path": "/healthz?js-enabled-only=true", + "port": 8222 + } + }, + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/tls", + "name": "tls", + "readOnly": true + }, + { + "mountPath": "/data", + "name": "data" + } + ] + } + ], + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "name": "config", + "secret": { + "secretName": "internal-dns-nats-config" + } + }, + { + "name": "tls", + "secret": { + "secretName": "internal-dns-nats-server" + } + } + ] + } + }, + "volumeClaimTemplates": [ + { + "metadata": { + "name": "data" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1Gi" + } + } + } + } + ] + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "internal-dns-member": "node-us-central1-0", + "internal-dns-role": "node" + }, + "name": "internal-dns-node", + "namespace": "internal-dns-system" + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=agent --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "agent", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=watchdog --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "watchdog", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/usr/local/sbin/internal-dns-daemons", + "node" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "serving", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [ + "NET_BIND_SERVICE" + ], + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + } + ], + "initContainers": [ + { + "command": [ + "/bin/sh", + "-ec", + "ip -6 route replace local fd53::/64 dev lo" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "destination-fixture", + "securityContext": { + "capabilities": { + "add": [ + "NET_ADMIN" + ], + "drop": [ + "ALL" + ] + }, + "runAsUser": 0 + } + } + ], + "securityContext": { + "fsGroup": 65532, + "fsGroupChangePolicy": "OnRootMismatch", + "sysctls": [ + { + "name": "net.ipv4.ip_unprivileged_port_start", + "value": "0" + } + ] + }, + "serviceAccountName": "internal-dns-fleet", + "shareProcessNamespace": true, + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "internal-dns-node-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "node-us-central1-0-nats" + } + }, + { + "secret": { + "name": "node-us-central1-0-password" + } + } + ] + } + }, + { + "name": "state", + "persistentVolumeClaim": { + "claimName": "internal-dns-node-state" + } + }, + { + "name": "daemon-config", + "persistentVolumeClaim": { + "claimName": "internal-dns-node-config" + } + }, + { + "emptyDir": { + "medium": "Memory" + }, + "name": "lease" + }, + { + "emptyDir": {}, + "name": "runtime" + }, + { + "name": "rndc", + "secret": { + "secretName": "internal-dns-rndc" + } + } + ] + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-node-state", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-node-config", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "internal-dns-member": "regional-front-us-central1-0", + "internal-dns-role": "regional-dnsdist" + }, + "name": "internal-dns-regional-front", + "namespace": "internal-dns-system" + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=agent --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "agent", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=watchdog --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "watchdog", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/usr/local/sbin/internal-dns-daemons", + "regional-dnsdist" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "serving", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [ + "NET_BIND_SERVICE" + ], + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + } + ], + "initContainers": [ + { + "command": [ + "/bin/sh", + "-ec", + "ip -6 route replace local fd54::/64 dev lo" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "destination-fixture", + "securityContext": { + "capabilities": { + "add": [ + "NET_ADMIN" + ], + "drop": [ + "ALL" + ] + }, + "runAsUser": 0 + } + } + ], + "securityContext": { + "fsGroup": 65532, + "fsGroupChangePolicy": "OnRootMismatch", + "sysctls": [ + { + "name": "net.ipv4.ip_unprivileged_port_start", + "value": "0" + } + ] + }, + "serviceAccountName": "internal-dns-fleet", + "shareProcessNamespace": true, + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "internal-dns-regional-front-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "regional-front-us-central1-0-nats" + } + }, + { + "secret": { + "name": "regional-front-us-central1-0-password" + } + } + ] + } + }, + { + "name": "state", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-front-state" + } + }, + { + "name": "daemon-config", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-front-config" + } + }, + { + "emptyDir": { + "medium": "Memory" + }, + "name": "lease" + }, + { + "emptyDir": {}, + "name": "runtime" + }, + { + "name": "rndc", + "secret": { + "secretName": "internal-dns-rndc" + } + } + ] + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-front-state", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-front-config", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "internal-dns-member": "regional-bind-us-central1-0", + "internal-dns-role": "regional-bind" + }, + "name": "internal-dns-regional-bind-0", + "namespace": "internal-dns-system" + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=agent --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "agent", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=watchdog --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "watchdog", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/usr/local/sbin/internal-dns-daemons", + "regional-bind" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "serving", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [ + "NET_BIND_SERVICE" + ], + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + } + ], + "initContainers": [], + "securityContext": { + "fsGroup": 65532, + "fsGroupChangePolicy": "OnRootMismatch", + "sysctls": [ + { + "name": "net.ipv4.ip_unprivileged_port_start", + "value": "0" + } + ] + }, + "serviceAccountName": "internal-dns-fleet", + "shareProcessNamespace": true, + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "internal-dns-regional-bind-0-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "regional-bind-us-central1-0-nats" + } + }, + { + "secret": { + "name": "regional-bind-us-central1-0-password" + } + } + ] + } + }, + { + "name": "state", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-bind-0-state" + } + }, + { + "name": "daemon-config", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-bind-0-config" + } + }, + { + "emptyDir": { + "medium": "Memory" + }, + "name": "lease" + }, + { + "emptyDir": {}, + "name": "runtime" + }, + { + "name": "rndc", + "secret": { + "secretName": "internal-dns-rndc" + } + } + ] + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-bind-0-state", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-bind-0-config", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "internal-dns-member": "regional-bind-us-central1-1", + "internal-dns-role": "regional-bind" + }, + "name": "internal-dns-regional-bind-1", + "namespace": "internal-dns-system" + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=agent --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "agent", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ]; do sleep 1; done; exec /internal-dns --role=watchdog --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "watchdog", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "50m", + "memory": "64Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + }, + { + "command": [ + "/usr/local/sbin/internal-dns-daemons", + "regional-bind" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "serving", + "resources": { + "limits": { + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [ + "NET_BIND_SERVICE" + ], + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/lib/internal-dns", + "name": "state", + "readOnly": false + }, + { + "mountPath": "/etc/internal-dns", + "name": "daemon-config", + "readOnly": false + }, + { + "mountPath": "/run/internal-dns", + "name": "lease", + "readOnly": false + }, + { + "mountPath": "/tmp", + "name": "runtime", + "readOnly": false + }, + { + "mountPath": "/etc/bind", + "name": "rndc", + "readOnly": true + } + ] + } + ], + "initContainers": [], + "securityContext": { + "fsGroup": 65532, + "fsGroupChangePolicy": "OnRootMismatch", + "sysctls": [ + { + "name": "net.ipv4.ip_unprivileged_port_start", + "value": "0" + } + ] + }, + "serviceAccountName": "internal-dns-fleet", + "shareProcessNamespace": true, + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "internal-dns-regional-bind-1-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "regional-bind-us-central1-1-nats" + } + }, + { + "secret": { + "name": "regional-bind-us-central1-1-password" + } + } + ] + } + }, + { + "name": "state", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-bind-1-state" + } + }, + { + "name": "daemon-config", + "persistentVolumeClaim": { + "claimName": "internal-dns-regional-bind-1-config" + } + }, + { + "emptyDir": { + "medium": "Memory" + }, + "name": "lease" + }, + { + "emptyDir": {}, + "name": "runtime" + }, + { + "name": "rndc", + "secret": { + "secretName": "internal-dns-rndc" + } + } + ] + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-bind-1-state", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "internal-dns-regional-bind-1-config", + "namespace": "internal-dns-system" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "512Mi" + } + } + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "internal-dns-admission", + "namespace": "internal-dns-system" + }, + "spec": { + "ports": [ + { + "name": "https", + "nodePort": 30443, + "port": 443, + "targetPort": 9443 + } + ], + "selector": { + "internal-dns-role": "control-plane" + }, + "type": "NodePort" + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "control-a", + "namespace": "internal-dns-system" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "control-a" + } + }, + "template": { + "metadata": { + "labels": { + "app": "control-a", + "internal-dns-role": "control-plane" + } + }, + "spec": { + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ] || [ ! -s /sources/project-a.json ]; do sleep 1; done; exec /internal-dns --role=control-plane --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "control-plane", + "ports": [ + { + "containerPort": 9443, + "name": "admission" + } + ], + "readinessProbe": { + "tcpSocket": { + "port": 9443 + } + }, + "resources": { + "limits": { + "memory": "768Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/admission", + "name": "admission", + "readOnly": true + }, + { + "mountPath": "/sources", + "name": "sources", + "readOnly": true + } + ] + } + ], + "serviceAccountName": "internal-dns-control-plane", + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "control-a-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "control-a-nats" + } + }, + { + "secret": { + "name": "control-a-password" + } + } + ] + } + }, + { + "name": "admission", + "secret": { + "secretName": "internal-dns-admission-tls" + } + }, + { + "name": "sources", + "secret": { + "optional": true, + "secretName": "internal-dns-sources" + } + } + ] + } + } + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "control-b", + "namespace": "internal-dns-system" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "control-b" + } + }, + "template": { + "metadata": { + "labels": { + "app": "control-b", + "internal-dns-role": "control-plane" + } + }, + "spec": { + "containers": [ + { + "command": [ + "/bin/sh", + "-ec", + "while [ ! -s /config/config.json ] || [ ! -s /sources/project-a.json ]; do sleep 1; done; exec /internal-dns --role=control-plane --config=/config/config.json" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "control-plane", + "ports": [ + { + "containerPort": 9443, + "name": "admission" + } + ], + "readinessProbe": { + "tcpSocket": { + "port": 9443 + } + }, + "resources": { + "limits": { + "memory": "768Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true, + "runAsGroup": 65532, + "runAsNonRoot": true, + "runAsUser": 65532 + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/nats", + "name": "nats", + "readOnly": true + }, + { + "mountPath": "/var/run/internal-dns/admission", + "name": "admission", + "readOnly": true + }, + { + "mountPath": "/sources", + "name": "sources", + "readOnly": true + } + ] + } + ], + "serviceAccountName": "internal-dns-control-plane", + "terminationGracePeriodSeconds": 2, + "volumes": [ + { + "configMap": { + "name": "control-b-config", + "optional": true + }, + "name": "config" + }, + { + "name": "nats", + "projected": { + "sources": [ + { + "secret": { + "name": "control-b-nats" + } + }, + { + "secret": { + "name": "control-b-password" + } + } + ] + } + }, + { + "name": "admission", + "secret": { + "secretName": "internal-dns-admission-tls" + } + }, + { + "name": "sources", + "secret": { + "optional": true, + "secretName": "internal-dns-sources" + } + } + ] + } + } + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "internal-dns-probe", + "namespace": "internal-dns-system" + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/bin/sh", + "-c", + "sleep infinity" + ], + "image": "internal-dns-kubernetes:e2e", + "imagePullPolicy": "Never", + "name": "probe", + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "readOnlyRootFilesystem": true + } + } + ], + "securityContext": { + "runAsGroup": 65532, + "runAsUser": 65532 + }, + "terminationGracePeriodSeconds": 2 + } + } + ], + "kind": "List" +} diff --git a/.internal-dns-e2e/kubernetes/platform-internal.kubeconfig b/.internal-dns-e2e/kubernetes/platform-internal.kubeconfig new file mode 100644 index 00000000..33b19f20 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/platform-internal.kubeconfig @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSkxHMktNYnFReHN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUwTlRkYUZ3MHpOakV3TURreU1qVTVOVGRhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUREaVdFVXNKSEN4S1Z3aWY3YkJXeTk3TDlRejE3WDBQakZqRE1MYWNta2lMZThDOTJoc1JmdW84NUEKeG02aGhyYXg2VG83cXREbUpHRXpqdVpEYittbnlubDc5c2Q0VEZ0M3JpZlQ0ZFZqalVvN0RHOUlwczhRZHdaZQo1ZGFCdVNNc0RPcUY4ayt1WXEvRmJnZFM3bjREcGt2dzdGNWpJTlpINWM3clRjck9sM01JZkVYVG4ySDBvdzZKCjhGeW4zdDBpUFkvRTl6U2NkRDgwRDJtZmxkVVh2VU0zWmpodDV0NVFwUkpIYzQ1cVY2MkFjYkxjV2hMZEE5UTUKS3hqVEx2aHZMY3dyZ0pNOW5iWmgrOTlwb2k5QU56dlFmU2I3U1A3OXVtL2VKRFZYUWEwTzJkYyttcnl6amk1dAorRHFOdVhNU0dsR0ltYnNZVWpoaldHSUxHaXR0QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRVzl1d2N4ckp4a3VXNHFZR1Y0Y1lNSFJ0N05UQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQkg1R2xheVh2VgpLWmYwaFdsYit0UC9wOThobWR2Z1ZVcEVsOVhzM21LMFUwc1QwdHhqbzVBbUZzYzBZTmhyL05JdGdwK0VGWG1yCnFXQis5YU4ydVMxSmdsYjZRNWlJZVpWbk1BcDk1ZkZSenZFR200eGNVbGFHVUFpelJMVlN3OTFGVmRyOFBPb0sKTFUvMnlndElnZUZheVBqY0NoS2V1dHg2WklHZWpGQWZlemxkTVpKV092YXNvV2o2Y2JVS2ZoZ0ZFUnp1VHE0Nwo3QTEwcmovZ2phWWRzWldzcnovQ0E0SlZqYzlkYmtXd2tOTVVmZlR3TFlaMDJsUVB4ejgvdHpIaGZ0SEp6ZXg2CnhoMmJPZVhkM3ZKc3pwcXE0TDdjV0dRRlY4VUtTQmVTYnY4R3ZWUUcrU0pQeEl0ZW1NcGt0Y0tBMklIZ2FQWU4KZHo0RWpEclp4UEdICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://dns-control-control-plane:6443 + name: kind-dns-control +contexts: +- context: + cluster: kind-dns-control + user: kind-dns-control + name: kind-dns-control +current-context: kind-dns-control +kind: Config +users: +- name: kind-dns-control + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJTUJEUmQwbUZxTmd3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUwTlRkYUZ3MHlOekV3TVRJeU1qVTVOVGRhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFETzFndmMKbEd2RlR5VW52cm5RZnBPWGh4ZUw0dFBNeUFHenJtdHFqR1BuL3l4b0psWEZORTlxUHdpdGRjRncyUi9sd1JJTQpFQmh6QlozTmx3dGM3UXBnaDY3WXpVdUVKZmNXZUVRQmlxY1hpNXpFZE5EM0tjVFd2dUtYYzU3MFRRU21kSUZ0CnhLVC82N3RQd21vcm94QWh3bVZqVlJIcEZQQzgvdjNhUlBINE1raEQ4ZEtmSHptN25oZWNLTHhpN3ZiVmZsbTMKSUhvUjhwRytBaXcxRDhPWjZwaHFZSGZTTGF0UmJLWkM3NjJCNXpYYzZLNFBEOTYyQm52c01sV0hMSi8zOWg1dgpLWUd6dGV5a2VsckNqdmJxUjJsOTVTeHJxRHRmbEY3Z2k3WVRFMlVEbERKMm9OTE1FS0t6bDFXNHBpVnZMMUhLCnJDUE5hMXMxalZibFZ2VzlBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkJiMjdCekdzbkdTNWJpcApnWlhoeGd3ZEczczFNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUN0YXdHLy9xWTZ0ZXArQUNyb05uMmNlNHFFCitRaDlRbmlXbSt3Sk1BbGRZWFFOOVJDQkVab2djUmltcldzcnNXTGhUVE1HSm9JczN3dzlEMlp5Rm1MNVFpcmkKUHNaM2J1YUdBaTJORlNZTmJVc0Fpa0pzd2lndk1YanVOWGg4SnQ4cll3ZWZPeDUvVWE3RE12REhZakVnOHdCOQo1NTMvSWVmZVQrOThoc0tNNWtOSHhVWEttYWhLWUlENEJ5QkFrdnRBaWoyNHZ6b053K0VtTUJqRXpXTkdBQlRzCjFzc0VpQWdVaDNtaXNyLzhyaktDZVNTRjVmTkU1UmRFdkRYc3lIYXBhenQxL01UbVh4UzBvamlISnlRb3JBajIKZGRkaGV6TTV6cHMrV0RWUkU0V0Y2L1lQcDExT3VEQVlLbUdvMXNYMVZGWnFVVmw3Y0Q5UGdCSUd3SjRICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBenRZTDNKUnJ4VThsSjc2NTBINlRsNGNYaStMVHpNZ0JzNjVyYW94ajUvOHNhQ1pWCnhUUlBhajhJclhYQmNOa2Y1Y0VTREJBWWN3V2R6WmNMWE8wS1lJZXUyTTFMaENYM0ZuaEVBWXFuRjR1Y3hIVFEKOXluRTFyN2lsM09lOUUwRXBuU0JiY1NrLyt1N1Q4SnFLNk1RSWNKbFkxVVI2UlR3dlA3OTJrVHgrREpJUS9IUwpueDg1dTU0WG5DaThZdTcyMVg1WnR5QjZFZktSdmdJc05RL0RtZXFZYW1CMzBpMnJVV3ltUXUrdGdlYzEzT2l1CkR3L2V0Z1o3N0RKVmh5eWY5L1llYnltQnM3WHNwSHBhd283MjZrZHBmZVVzYTZnN1g1UmU0SXUyRXhObEE1UXkKZHFEU3pCQ2lzNWRWdUtZbGJ5OVJ5cXdqeld0Yk5ZMVc1VmIxdlFJREFRQUJBb0lCQUFQYTE3Q2I0bVVyU3dVZwpDa3VwVk5VbFI3RVFseXpPdkZLRytXRDFqcFg1TFBDeTFqU3RSSjdIdXh3K041ZmZIVGVsanZYNkJhenFpK0dCCjQzRXZFY2tEV2NsemVnMnZCbEFxNVJLMy94dUU4bEJ6bHpCSXBpV25CUm1HRkNDY1Z0dlpEQVl5MkZHTGx1U3YKRlhybjNSbjJRZFBGaVVIckcvNFM0Nis0M1RyRHlrSjBkUGp6QjlFYXpJcE5senR4Rk9XZ0xPRDd1YklsQnR0MQpreFVyY0l0SFlPTEczTmNHTVJqb25heDE1VHdjL0hTU2JnWERYam9kcU92SWdKSTFXdE5FTW5xMDkrcXdXdlJ3CmlFUllqdUFsRDgxa2RJTWhZQ0VsNzhrREl4K0NmZ3l3MHY1ekZmMy9nUVJoZFoyeDU5UjZaUGN5NG1vdlBjTi8KV3dIeDRsa0NnWUVBOWoweFVKeXVUdHp0c2hZM2ZRMHVOUUxrS3A2TlcvKzdxOEZpVVIzS08zVUg4ZHo3Q2xhMgpZeUUxd1h6Q1BoNmI4K1RlV09xbTJQeVN3RmtURWlBdUsyeVd0RDBXL1lFLzdMckgvbXc2dnIzb0wraWt0Q0R6CkxNWDdRWDBCejNNcTg4eXlSSDJFT0RGMndnQVl3eW5HdG83TW4yV1dWOTgrUWRxMW9ycTI5U01DZ1lFQTF3ai8KUS9teWtYNzhsUlF5Y0FzRFZnRTB3WkVOYjVsbURrNVZqQkduSkNzaElreEwwWE5ucjlyU1NiRGp1Um9oWm1rWAoreWhQZzZXWU9VUWpNNEVQQVgrcEx5UWRsWFNUSGh0aktyL28xU2k3ZExaUUxlSmF5ZXlWQnRxRE9ibjZVZU1hClNTWEVpd3Z5RFdYbjZFN2w4SzZ5Q2VqTXBBRDkzTGZZamxqb1I1OENnWUJEQjRWVkhGR1d4aTB1UFQzOW5Jam4KaFZIM0NCSVM2UnA5TnJjOHZZOC9nQ3daZXI0Ym9sOGp2UGFWSlJEN04vbmIrb0ZNVXhQNEhVa1ZjeW50MVRTOApQV01JelZoa3licE5Cc3RLUnN2M0FGc2tMNEFtU0pIcHJqc25LSkJGbUdRbzJaL3RaUDJKRG0zMWdYRThMek1ZCm51YTB2VUtmeVBWNWw3KzUvSG14WVFLQmdCK3FpSVRIR1ovSmRvS3hwYmZRTjE2TDhPdnNZM0Vpd3QrcWMzaWMKQk9RVG9kNStJOUFsVVlpWkRjeWlTdmF3UFdqcVl3aXVaLzgzL0pHNlVnQzhxYXdMMDZ0SWd6N0dHd0FkQWw2QwpRbFJabWpsV3U3bjJFbTZYODE0K0FZTVh1eUFoOUhYL0pORmhJZlVOSTdpWEhjOXl1bTh4SkNtbXA3dkFLeGpUClNPbDdBb0dBWVdqZ1ZNdDBVbVlpaFBzT0VIV0tubkFrT3pQREJYV0pzZS84NDBnK2g5RUd0R1FiQnR4NDdsb3cKZnpQY3FFK0k5WXJxamxjc1pzZUF4NTRMSldTdXVlK0lHRXZPWldvWExiSGUxVDZrUzc3QnYxa1NaUHJoWmo4agoxWmZQR3dNNHJFUDQ0b3ovVThnckdhejhwZ2U0d0c0dUdvMWNnV2M1bE1RR1MrSktiNDQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + diff --git a/.internal-dns-e2e/kubernetes/platform.kubeconfig b/.internal-dns-e2e/kubernetes/platform.kubeconfig new file mode 100644 index 00000000..2889e57a --- /dev/null +++ b/.internal-dns-e2e/kubernetes/platform.kubeconfig @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSkxHMktNYnFReHN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUwTlRkYUZ3MHpOakV3TURreU1qVTVOVGRhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUREaVdFVXNKSEN4S1Z3aWY3YkJXeTk3TDlRejE3WDBQakZqRE1MYWNta2lMZThDOTJoc1JmdW84NUEKeG02aGhyYXg2VG83cXREbUpHRXpqdVpEYittbnlubDc5c2Q0VEZ0M3JpZlQ0ZFZqalVvN0RHOUlwczhRZHdaZQo1ZGFCdVNNc0RPcUY4ayt1WXEvRmJnZFM3bjREcGt2dzdGNWpJTlpINWM3clRjck9sM01JZkVYVG4ySDBvdzZKCjhGeW4zdDBpUFkvRTl6U2NkRDgwRDJtZmxkVVh2VU0zWmpodDV0NVFwUkpIYzQ1cVY2MkFjYkxjV2hMZEE5UTUKS3hqVEx2aHZMY3dyZ0pNOW5iWmgrOTlwb2k5QU56dlFmU2I3U1A3OXVtL2VKRFZYUWEwTzJkYyttcnl6amk1dAorRHFOdVhNU0dsR0ltYnNZVWpoaldHSUxHaXR0QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRVzl1d2N4ckp4a3VXNHFZR1Y0Y1lNSFJ0N05UQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQkg1R2xheVh2VgpLWmYwaFdsYit0UC9wOThobWR2Z1ZVcEVsOVhzM21LMFUwc1QwdHhqbzVBbUZzYzBZTmhyL05JdGdwK0VGWG1yCnFXQis5YU4ydVMxSmdsYjZRNWlJZVpWbk1BcDk1ZkZSenZFR200eGNVbGFHVUFpelJMVlN3OTFGVmRyOFBPb0sKTFUvMnlndElnZUZheVBqY0NoS2V1dHg2WklHZWpGQWZlemxkTVpKV092YXNvV2o2Y2JVS2ZoZ0ZFUnp1VHE0Nwo3QTEwcmovZ2phWWRzWldzcnovQ0E0SlZqYzlkYmtXd2tOTVVmZlR3TFlaMDJsUVB4ejgvdHpIaGZ0SEp6ZXg2CnhoMmJPZVhkM3ZKc3pwcXE0TDdjV0dRRlY4VUtTQmVTYnY4R3ZWUUcrU0pQeEl0ZW1NcGt0Y0tBMklIZ2FQWU4KZHo0RWpEclp4UEdICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:59799 + name: kind-dns-control +contexts: +- context: + cluster: kind-dns-control + user: kind-dns-control + name: kind-dns-control +current-context: kind-dns-control +kind: Config +users: +- name: kind-dns-control + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJTUJEUmQwbUZxTmd3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUwTlRkYUZ3MHlOekV3TVRJeU1qVTVOVGRhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFETzFndmMKbEd2RlR5VW52cm5RZnBPWGh4ZUw0dFBNeUFHenJtdHFqR1BuL3l4b0psWEZORTlxUHdpdGRjRncyUi9sd1JJTQpFQmh6QlozTmx3dGM3UXBnaDY3WXpVdUVKZmNXZUVRQmlxY1hpNXpFZE5EM0tjVFd2dUtYYzU3MFRRU21kSUZ0CnhLVC82N3RQd21vcm94QWh3bVZqVlJIcEZQQzgvdjNhUlBINE1raEQ4ZEtmSHptN25oZWNLTHhpN3ZiVmZsbTMKSUhvUjhwRytBaXcxRDhPWjZwaHFZSGZTTGF0UmJLWkM3NjJCNXpYYzZLNFBEOTYyQm52c01sV0hMSi8zOWg1dgpLWUd6dGV5a2VsckNqdmJxUjJsOTVTeHJxRHRmbEY3Z2k3WVRFMlVEbERKMm9OTE1FS0t6bDFXNHBpVnZMMUhLCnJDUE5hMXMxalZibFZ2VzlBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkJiMjdCekdzbkdTNWJpcApnWlhoeGd3ZEczczFNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUN0YXdHLy9xWTZ0ZXArQUNyb05uMmNlNHFFCitRaDlRbmlXbSt3Sk1BbGRZWFFOOVJDQkVab2djUmltcldzcnNXTGhUVE1HSm9JczN3dzlEMlp5Rm1MNVFpcmkKUHNaM2J1YUdBaTJORlNZTmJVc0Fpa0pzd2lndk1YanVOWGg4SnQ4cll3ZWZPeDUvVWE3RE12REhZakVnOHdCOQo1NTMvSWVmZVQrOThoc0tNNWtOSHhVWEttYWhLWUlENEJ5QkFrdnRBaWoyNHZ6b053K0VtTUJqRXpXTkdBQlRzCjFzc0VpQWdVaDNtaXNyLzhyaktDZVNTRjVmTkU1UmRFdkRYc3lIYXBhenQxL01UbVh4UzBvamlISnlRb3JBajIKZGRkaGV6TTV6cHMrV0RWUkU0V0Y2L1lQcDExT3VEQVlLbUdvMXNYMVZGWnFVVmw3Y0Q5UGdCSUd3SjRICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBenRZTDNKUnJ4VThsSjc2NTBINlRsNGNYaStMVHpNZ0JzNjVyYW94ajUvOHNhQ1pWCnhUUlBhajhJclhYQmNOa2Y1Y0VTREJBWWN3V2R6WmNMWE8wS1lJZXUyTTFMaENYM0ZuaEVBWXFuRjR1Y3hIVFEKOXluRTFyN2lsM09lOUUwRXBuU0JiY1NrLyt1N1Q4SnFLNk1RSWNKbFkxVVI2UlR3dlA3OTJrVHgrREpJUS9IUwpueDg1dTU0WG5DaThZdTcyMVg1WnR5QjZFZktSdmdJc05RL0RtZXFZYW1CMzBpMnJVV3ltUXUrdGdlYzEzT2l1CkR3L2V0Z1o3N0RKVmh5eWY5L1llYnltQnM3WHNwSHBhd283MjZrZHBmZVVzYTZnN1g1UmU0SXUyRXhObEE1UXkKZHFEU3pCQ2lzNWRWdUtZbGJ5OVJ5cXdqeld0Yk5ZMVc1VmIxdlFJREFRQUJBb0lCQUFQYTE3Q2I0bVVyU3dVZwpDa3VwVk5VbFI3RVFseXpPdkZLRytXRDFqcFg1TFBDeTFqU3RSSjdIdXh3K041ZmZIVGVsanZYNkJhenFpK0dCCjQzRXZFY2tEV2NsemVnMnZCbEFxNVJLMy94dUU4bEJ6bHpCSXBpV25CUm1HRkNDY1Z0dlpEQVl5MkZHTGx1U3YKRlhybjNSbjJRZFBGaVVIckcvNFM0Nis0M1RyRHlrSjBkUGp6QjlFYXpJcE5senR4Rk9XZ0xPRDd1YklsQnR0MQpreFVyY0l0SFlPTEczTmNHTVJqb25heDE1VHdjL0hTU2JnWERYam9kcU92SWdKSTFXdE5FTW5xMDkrcXdXdlJ3CmlFUllqdUFsRDgxa2RJTWhZQ0VsNzhrREl4K0NmZ3l3MHY1ekZmMy9nUVJoZFoyeDU5UjZaUGN5NG1vdlBjTi8KV3dIeDRsa0NnWUVBOWoweFVKeXVUdHp0c2hZM2ZRMHVOUUxrS3A2TlcvKzdxOEZpVVIzS08zVUg4ZHo3Q2xhMgpZeUUxd1h6Q1BoNmI4K1RlV09xbTJQeVN3RmtURWlBdUsyeVd0RDBXL1lFLzdMckgvbXc2dnIzb0wraWt0Q0R6CkxNWDdRWDBCejNNcTg4eXlSSDJFT0RGMndnQVl3eW5HdG83TW4yV1dWOTgrUWRxMW9ycTI5U01DZ1lFQTF3ai8KUS9teWtYNzhsUlF5Y0FzRFZnRTB3WkVOYjVsbURrNVZqQkduSkNzaElreEwwWE5ucjlyU1NiRGp1Um9oWm1rWAoreWhQZzZXWU9VUWpNNEVQQVgrcEx5UWRsWFNUSGh0aktyL28xU2k3ZExaUUxlSmF5ZXlWQnRxRE9ibjZVZU1hClNTWEVpd3Z5RFdYbjZFN2w4SzZ5Q2VqTXBBRDkzTGZZamxqb1I1OENnWUJEQjRWVkhGR1d4aTB1UFQzOW5Jam4KaFZIM0NCSVM2UnA5TnJjOHZZOC9nQ3daZXI0Ym9sOGp2UGFWSlJEN04vbmIrb0ZNVXhQNEhVa1ZjeW50MVRTOApQV01JelZoa3licE5Cc3RLUnN2M0FGc2tMNEFtU0pIcHJqc25LSkJGbUdRbzJaL3RaUDJKRG0zMWdYRThMek1ZCm51YTB2VUtmeVBWNWw3KzUvSG14WVFLQmdCK3FpSVRIR1ovSmRvS3hwYmZRTjE2TDhPdnNZM0Vpd3QrcWMzaWMKQk9RVG9kNStJOUFsVVlpWkRjeWlTdmF3UFdqcVl3aXVaLzgzL0pHNlVnQzhxYXdMMDZ0SWd6N0dHd0FkQWw2QwpRbFJabWpsV3U3bjJFbTZYODE0K0FZTVh1eUFoOUhYL0pORmhJZlVOSTdpWEhjOXl1bTh4SkNtbXA3dkFLeGpUClNPbDdBb0dBWVdqZ1ZNdDBVbVlpaFBzT0VIV0tubkFrT3pQREJYV0pzZS84NDBnK2g5RUd0R1FiQnR4NDdsb3cKZnpQY3FFK0k5WXJxamxjc1pzZUF4NTRMSldTdXVlK0lHRXZPWldvWExiSGUxVDZrUzc3QnYxa1NaUHJoWmo4agoxWmZQR3dNNHJFUDQ0b3ovVThnckdhejhwZ2U0d0c0dUdvMWNnV2M1bE1RR1MrSktiNDQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/.internal-dns-e2e/kubernetes/project-a-bootstrap.json b/.internal-dns-e2e/kubernetes/project-a-bootstrap.json new file mode 100644 index 00000000..228cb7ea --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-a-bootstrap.json @@ -0,0 +1,946 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "project-e2e" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "compute-system" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "network-system" + } + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "automountServiceAccountToken": false, + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-fleet", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-vpc-integration", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status", + "dnsmanagednamespaces", + "dnsmanagednamespaces/status" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-product-writer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations", + "dnsrecordcontributions" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants", + "dnsresolvercontexts", + "dnsmanagednamespaces", + "dnsnamingpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsrecordcontributions/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-customer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsnamingpolicies", + "dnsregistrations" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-grant-issuer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "internal-dns-control-plane" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsregistrations", + "dnscontributiongrants", + "dnsrecordcontributions", + "dnsnamingpolicies", + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnszoneassociations" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings", + "dnspublicationmanifests", + "dnspublicationchunks", + "dnstransportoutboxes", + "dnspublicationownerships", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings/status", + "dnspublicationmanifests/status", + "dnstransportoutboxes/status", + "dnspublicationownerships/status", + "dnsmanagednamespaces/status", + "dnszones/status", + "dnszoneassociations/status", + "dnsnamingpolicies/status", + "dnsregistrations/status", + "dnscontributiongrants/status", + "dnsrecordcontributions/status", + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "internal-dns-source-worker", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "internal-dns-control-plane" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-customer", + "namespace": "project-e2e" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-customer", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-customer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-customer-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-a" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-customer-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-customer-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-customer-self", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-customer" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-customer-self", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-customer-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-publisher", + "namespace": "compute-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-publisher", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-product-writer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-publisher-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-a" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-publisher-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-publisher-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-publisher-self", + "namespace": "compute-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-publisher" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-publisher-self", + "namespace": "compute-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-publisher-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-grant-issuer", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-grant-issuer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-grant-issuer-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-a" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-grant-issuer-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-grant-issuer-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-grant-issuer-self", + "namespace": "compute-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-grant-issuer" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-grant-issuer-self", + "namespace": "compute-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-grant-issuer-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-vpc-integration", + "namespace": "network-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-vpc-integration", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-vpc-integration" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-vpc-integration-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-a" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-vpc-integration-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-vpc-integration-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-vpc-integration-self", + "namespace": "network-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-vpc-integration" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-vpc-integration-self", + "namespace": "network-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-vpc-integration-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "internal-dns-control-plane-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-a" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "internal-dns-control-plane-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "internal-dns-control-plane-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-control-plane-self", + "namespace": "internal-dns-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "internal-dns-control-plane" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "internal-dns-control-plane-self", + "namespace": "internal-dns-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-control-plane-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "dns.networking.miloapis.com/v1alpha1", + "kind": "DNSZoneClass", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "private-bind" + }, + "spec": { + "controllerName": "bind", + "nameServerPolicy": { + "mode": "Static", + "static": { + "servers": [ + "ns.internal." + ] + } + } + } + } + ], + "kind": "List" +} diff --git a/.internal-dns-e2e/kubernetes/project-a-internal.kubeconfig b/.internal-dns-e2e/kubernetes/project-a-internal.kubeconfig new file mode 100644 index 00000000..07ced9a1 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-a-internal.kubeconfig @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJVVArK3dzVkpTWDR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUzTURaYUZ3MHpOakV3TURreU16QXlNRFphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUN1ck9uQy9yd2ZCaWU3Ky9pbmZOMUFNRHZaaXQvMWxKTzhvNFpRRmp0K3E4VHJLcEJhUERVWEFUeHgKNHpzREk3QVYydFRXMWJTZEVLbmZoN0ViM2hKSGpnZXFvckUwS0hJUENMM0N1TlhlZ3FqWVMzOHN0a2dMbWpSWgpidkRCMWJXejVYd3licUFMSVBZM2txckJ1aW1jVjJuclREdW83YUEybmVCZ3Fzc1QxSjhzblRMN1dodDd6TWlGCnM4WjQ5NWI1ODlYcFNGWlFtbXdEdnNGQm5oUTJIQUJQK0E0R2svdWdUWkhQMm1uZlhXQjl1K3VCUUJPakEyU24KNUh2Z1hZUmF5cjBPTU04Q0dXMzU0Q3ZHenhKbkdjN2Y4ME53a25veXU5WVVSVzR0VEZQOWhrUGVNMUo4T2lsbQp0SGZ0RFNQWkJWNlRLdjlWV3l3bmM5aTcyMWNqQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRSWlMbDhhdWRmUFNiYkxmQ1JUNzZiRlkxazlEQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQlRaT1VubVF5dApmQnp3ZTJCSHJ6ckNKWGVVbWlRRUtBc3AweDFka3NZcXZCa2ZJVTJXZjVxektmb3ZhcXdIaG44YkQwdEtqc2ZHCmVQN2J1SmcwVVM2NFZJRmRuZEk2ajl3QjFXN0t6QzRQbWJnSUY3bER0eWJrNTFEMXlXTzZ5ZUIzUUdyUjdoQ2EKY3JMRjZyQjJpWlNBMnkyS1JpMFJUR05Ed0N3aGtXRUhFdHpoOEVyRkRoS2NjQmFua25LaGk3N3dDMDNrRjYvdgp2UkkyM1p5THhCMVFja0hneXVYc1ZHVlZrbE5BejU4VUp6cjR2RkI1M25JL2NYRHdMd1A0Q1p1UCttTFB1aXFmCmh5MmU5Z3hiZnF1ZFdiZzA4cm91Y2lzL0hQby83bG9JckZVbkV2eUVoTDcrclN5VWxLSWF4ODM4N3lmWk5qQzgKRUlxMjAvUFkvV1ZDCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://dns-upstream-control-plane:6443 + name: kind-dns-upstream +contexts: +- context: + cluster: kind-dns-upstream + user: kind-dns-upstream + name: kind-dns-upstream +current-context: kind-dns-upstream +kind: Config +users: +- name: kind-dns-upstream + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJUUwvbmZtdFdzUjR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUzTURaYUZ3MHlOekV3TVRJeU16QXlNRFphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDdk5uaGYKMk5xMFJGUys5MFVCOXBFL0tmZkw5TEZuYWRpRUhVaHpqNkRxZ0lHWDlmSC9oMklUT21NWTJJMU9VR1Z5eGVhbQpTWUZVNjFTWGdyVy95enl1MUNIMUd5VXdBMzVyR1IwK1RMZUtoZkQwQ1pwY0FVZ3R2N2JDaDFuZ3EyZEhGNEUwCjlwN245RWhpVno2ME1DV2Qzc0M1emhFb1BIVU0wQ1l2Sy9SazVOVmsrd1B5d3M2MVBaNjQ5NlVqYXh6cjN5dXEKbEVEM1F0WFhTdURxb3lJcVhZdlNBOUtTdjNrUEtOZnNrZVo1UVBiRGhPQmdGYVA0S3JHQ1dWNHJCa2lBSVdzWQo2TktzaWcrRHBUbTd5T2IwRUFDeWFMWWhOdjZEZXA5Nm5iNFhxa1dwRlBjTlh6Tno5KytpdG5QV0JJcHhxcCtCCitLUkdHWnF2cUtIVDYvY05BZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkFpSXVYeHE1MTg5SnRzdAo4SkZQdnBzVmpXVDBNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFRTHRybHp3RHdYU2hmOVNmYU1TQjgwWkhPCnEzdTY3Sm94V3IxN0grVFJ1RWJIVGJMeHI3bmk1aFVxOGRxL21iNEI3cEgxc2p1UllBbXFmZ3BLZFl6UE1EU0sKbk1nNUlrWjRLNnloampvRG5rTWgxVVNIb25XQVd2bXJoS2p3OGIvaEpkVUJsYjVYcUpZU0VDVWJWMHFobVk0Qgp5d2xlU0RuM21iNlhvWUsySnU1eTJaOWxVUHVtNVV6NlpFS2xxSU1vdVQvRDRvN2xTTDh6VlVTSkFKVEMvUmdaCnprSjlrNFlPZFRSRzNQRXJmNEpXRnpzTXY2UWd6c3AyTkhESzFiL0J4MVhYcmFySS8rWGh2cUJqWlFRbVFPUSsKTE5QSUFDNkdEckRUNkRYSVFLbDg2VXZWMDQxZVRvM1NLZ2JGWi9nWUdsY2hKTTA0T01MZ3hNcHpqSFp2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBcnpaNFg5amF0RVJVdnZkRkFmYVJQeW4zeS9TeFoyblloQjFJYzQrZzZvQ0JsL1h4Ci80ZGlFenBqR05pTlRsQmxjc1htcGttQlZPdFVsNEsxdjhzOHJ0UWg5UnNsTUFOK2F4a2RQa3kzaW9YdzlBbWEKWEFGSUxiKzJ3b2RaNEt0blJ4ZUJOUGFlNS9SSVlsYyt0REFsbmQ3QXVjNFJLRHgxRE5BbUx5djBaT1RWWlBzRAo4c0xPdFQyZXVQZWxJMnNjNjk4cnFwUkE5MExWMTByZzZxTWlLbDJMMGdQU2tyOTVEeWpYN0pIbWVVRDJ3NFRnCllCV2orQ3F4Z2xsZUt3WklnQ0ZyR09qU3JJb1BnNlU1dThqbTlCQUFzbWkySVRiK2czcWZlcDIrRjZwRnFSVDMKRFY4emMvZnZvclp6MWdTS2NhcWZnZmlrUmhtYXI2aWgwK3YzRFFJREFRQUJBb0lCQUNyQWVMaXlXaEw0UlZGZgpWa2czV0pOMzh3MmlCQk4rb1NVRFhDVk9XMjQ4c3VKWVVaY08rcUJFYzkraEgwR0hubWxWM29TZWxOOGFtTVpyClQxdWdCSXVJR1UwTU5aTzBsb2tkVktSWjVjTDRFYVlLSGM3clJTaGNpZ1NITUNnQXlxOXZEY2pWa0VwNnNackYKUFBpTWN2RkZZL015VTc5a21RN3FUeGJkZThHT0tTU3VDM1R3UXo0VnQrdVRIRi9sb2g5QlRuZTNyWmUzMVQ5bQo3MkNDd29WZG9QQTNYa2dMcENwWCtGSjA5TjBhZm44UW40SnZiMXAvWjAyL3ZqempjMys0N1UxcnZaVFVZWDVPCm80MmZMT1FmUzBPejB1TGtQVFdCVVZkUmhhQ3lvNDBSeFozTi9QNDI0eHVPNFpoQWV0L3psMklUSjA1WVF1QVcKdlJqYytpVUNnWUVBMUdZL2ZUc0Fudm9qZXRaNjFWMWV5dW1qNkt3T3FBeVRXUWpReGZIRk1DOEkvdlZLTWtuagpsN2laVnptZFFOYzMrSjl2L01xaDIyemIzUGM1NzBmNmVBT3dubzNVeDFTQk8rUlgyTEZuMzAyMXZmdEFpZi9SCkNDZVI4cWhZRDdVTEhaeE9oekpCeS9tTFlWcUF3aWc4aDBWVnA3eG8yZkViOUJVcjdxanhObHNDZ1lFQTB5NEoKUDI0UzFrQS81OFFtWlZSaVVnYkpoYXZkY3lVbzJqTUd6K0g4NS9sZjhadis2ODczczFRWHp3cFA5UTE3V2ZuRQpsQ0ZTNTMyYnJUa1BWQXV5cjZJQnhySEZlTm8wVFV1emZKcVhyU21DNnRONXRud2VpMmd0NnA4NHZ5elgyem8wCkdvamlCN0Z1MHV2ckFuVkl5V3Y1NjFhTDhTMkpHZ251bGIvdEZMY0NnWUVBblU2bEV3RFI4dXhidnF4L0NPdEEKTzVSUyt3NERSTjhiZWtPWExDTmw0Y3ZVSTlCVGhXOUlDcVNEblNCa29STkV0dlNPb25FSTgxUnFlZnI0ZzRtUgpyYlU5NkJBdldQbnpSUUpYTlh3ZTdEbkhzczhVKzUvbGx0NkhyMnFTM2Y4K0hrNFhzdlc3MlhBV2xLSE0zditoCjVLV3JUTEcwRGtZT1ovWU9OWklXSkNVQ2dZRUFoOFpaZThheHBiNjVWN0h0WGNqYlRoRy9ENHBvWHRmTlpEemEKYkYzSDFQMm9paGJaMTVMQi96SlovR3VNN09rMkhhdFFrRXk4bU1sUmhoWGhuUS9JV3RPMmdLRmJiVkFkd0hTagplY0dMTTNJMUVmWFZrMVpKN1NlV0hmc3c2UGxUNzBlRXFHckpxRmlVQUZoZVNMNVBoZkUrUmVqbklMRStMTmVsCjVmSDM1QkVDZ1lFQWd1dEJtUGxrWWYvMkovYlFaNmg4WnlLd25GYytUQy9IZGlVbzhFNjlCdXNiOGxQZXlYencKTHdlQXh2WThkNkNWeC9RTGZPV3h2NVJrQ29vR2xNV2orbmJOWFY3VnZ3V1lGVkdSczM2cldRMnpqZCsxZ1BkOApTQjlUZVNFK0lvU0QwNjRHU01uNml3cGZUWm11NEhCUDZhbTJYSkdaNWpNTXZReVVvMzhDNTFrPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + diff --git a/.internal-dns-e2e/kubernetes/project-a.kubeconfig b/.internal-dns-e2e/kubernetes/project-a.kubeconfig new file mode 100644 index 00000000..6a1bd2c6 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-a.kubeconfig @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJVVArK3dzVkpTWDR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUzTURaYUZ3MHpOakV3TURreU16QXlNRFphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUN1ck9uQy9yd2ZCaWU3Ky9pbmZOMUFNRHZaaXQvMWxKTzhvNFpRRmp0K3E4VHJLcEJhUERVWEFUeHgKNHpzREk3QVYydFRXMWJTZEVLbmZoN0ViM2hKSGpnZXFvckUwS0hJUENMM0N1TlhlZ3FqWVMzOHN0a2dMbWpSWgpidkRCMWJXejVYd3licUFMSVBZM2txckJ1aW1jVjJuclREdW83YUEybmVCZ3Fzc1QxSjhzblRMN1dodDd6TWlGCnM4WjQ5NWI1ODlYcFNGWlFtbXdEdnNGQm5oUTJIQUJQK0E0R2svdWdUWkhQMm1uZlhXQjl1K3VCUUJPakEyU24KNUh2Z1hZUmF5cjBPTU04Q0dXMzU0Q3ZHenhKbkdjN2Y4ME53a25veXU5WVVSVzR0VEZQOWhrUGVNMUo4T2lsbQp0SGZ0RFNQWkJWNlRLdjlWV3l3bmM5aTcyMWNqQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRSWlMbDhhdWRmUFNiYkxmQ1JUNzZiRlkxazlEQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQlRaT1VubVF5dApmQnp3ZTJCSHJ6ckNKWGVVbWlRRUtBc3AweDFka3NZcXZCa2ZJVTJXZjVxektmb3ZhcXdIaG44YkQwdEtqc2ZHCmVQN2J1SmcwVVM2NFZJRmRuZEk2ajl3QjFXN0t6QzRQbWJnSUY3bER0eWJrNTFEMXlXTzZ5ZUIzUUdyUjdoQ2EKY3JMRjZyQjJpWlNBMnkyS1JpMFJUR05Ed0N3aGtXRUhFdHpoOEVyRkRoS2NjQmFua25LaGk3N3dDMDNrRjYvdgp2UkkyM1p5THhCMVFja0hneXVYc1ZHVlZrbE5BejU4VUp6cjR2RkI1M25JL2NYRHdMd1A0Q1p1UCttTFB1aXFmCmh5MmU5Z3hiZnF1ZFdiZzA4cm91Y2lzL0hQby83bG9JckZVbkV2eUVoTDcrclN5VWxLSWF4ODM4N3lmWk5qQzgKRUlxMjAvUFkvV1ZDCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:61329 + name: kind-dns-upstream +contexts: +- context: + cluster: kind-dns-upstream + user: kind-dns-upstream + name: kind-dns-upstream +current-context: kind-dns-upstream +kind: Config +users: +- name: kind-dns-upstream + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJUUwvbmZtdFdzUjR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalUzTURaYUZ3MHlOekV3TVRJeU16QXlNRFphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDdk5uaGYKMk5xMFJGUys5MFVCOXBFL0tmZkw5TEZuYWRpRUhVaHpqNkRxZ0lHWDlmSC9oMklUT21NWTJJMU9VR1Z5eGVhbQpTWUZVNjFTWGdyVy95enl1MUNIMUd5VXdBMzVyR1IwK1RMZUtoZkQwQ1pwY0FVZ3R2N2JDaDFuZ3EyZEhGNEUwCjlwN245RWhpVno2ME1DV2Qzc0M1emhFb1BIVU0wQ1l2Sy9SazVOVmsrd1B5d3M2MVBaNjQ5NlVqYXh6cjN5dXEKbEVEM1F0WFhTdURxb3lJcVhZdlNBOUtTdjNrUEtOZnNrZVo1UVBiRGhPQmdGYVA0S3JHQ1dWNHJCa2lBSVdzWQo2TktzaWcrRHBUbTd5T2IwRUFDeWFMWWhOdjZEZXA5Nm5iNFhxa1dwRlBjTlh6Tno5KytpdG5QV0JJcHhxcCtCCitLUkdHWnF2cUtIVDYvY05BZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkFpSXVYeHE1MTg5SnRzdAo4SkZQdnBzVmpXVDBNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFRTHRybHp3RHdYU2hmOVNmYU1TQjgwWkhPCnEzdTY3Sm94V3IxN0grVFJ1RWJIVGJMeHI3bmk1aFVxOGRxL21iNEI3cEgxc2p1UllBbXFmZ3BLZFl6UE1EU0sKbk1nNUlrWjRLNnloampvRG5rTWgxVVNIb25XQVd2bXJoS2p3OGIvaEpkVUJsYjVYcUpZU0VDVWJWMHFobVk0Qgp5d2xlU0RuM21iNlhvWUsySnU1eTJaOWxVUHVtNVV6NlpFS2xxSU1vdVQvRDRvN2xTTDh6VlVTSkFKVEMvUmdaCnprSjlrNFlPZFRSRzNQRXJmNEpXRnpzTXY2UWd6c3AyTkhESzFiL0J4MVhYcmFySS8rWGh2cUJqWlFRbVFPUSsKTE5QSUFDNkdEckRUNkRYSVFLbDg2VXZWMDQxZVRvM1NLZ2JGWi9nWUdsY2hKTTA0T01MZ3hNcHpqSFp2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBcnpaNFg5amF0RVJVdnZkRkFmYVJQeW4zeS9TeFoyblloQjFJYzQrZzZvQ0JsL1h4Ci80ZGlFenBqR05pTlRsQmxjc1htcGttQlZPdFVsNEsxdjhzOHJ0UWg5UnNsTUFOK2F4a2RQa3kzaW9YdzlBbWEKWEFGSUxiKzJ3b2RaNEt0blJ4ZUJOUGFlNS9SSVlsYyt0REFsbmQ3QXVjNFJLRHgxRE5BbUx5djBaT1RWWlBzRAo4c0xPdFQyZXVQZWxJMnNjNjk4cnFwUkE5MExWMTByZzZxTWlLbDJMMGdQU2tyOTVEeWpYN0pIbWVVRDJ3NFRnCllCV2orQ3F4Z2xsZUt3WklnQ0ZyR09qU3JJb1BnNlU1dThqbTlCQUFzbWkySVRiK2czcWZlcDIrRjZwRnFSVDMKRFY4emMvZnZvclp6MWdTS2NhcWZnZmlrUmhtYXI2aWgwK3YzRFFJREFRQUJBb0lCQUNyQWVMaXlXaEw0UlZGZgpWa2czV0pOMzh3MmlCQk4rb1NVRFhDVk9XMjQ4c3VKWVVaY08rcUJFYzkraEgwR0hubWxWM29TZWxOOGFtTVpyClQxdWdCSXVJR1UwTU5aTzBsb2tkVktSWjVjTDRFYVlLSGM3clJTaGNpZ1NITUNnQXlxOXZEY2pWa0VwNnNackYKUFBpTWN2RkZZL015VTc5a21RN3FUeGJkZThHT0tTU3VDM1R3UXo0VnQrdVRIRi9sb2g5QlRuZTNyWmUzMVQ5bQo3MkNDd29WZG9QQTNYa2dMcENwWCtGSjA5TjBhZm44UW40SnZiMXAvWjAyL3ZqempjMys0N1UxcnZaVFVZWDVPCm80MmZMT1FmUzBPejB1TGtQVFdCVVZkUmhhQ3lvNDBSeFozTi9QNDI0eHVPNFpoQWV0L3psMklUSjA1WVF1QVcKdlJqYytpVUNnWUVBMUdZL2ZUc0Fudm9qZXRaNjFWMWV5dW1qNkt3T3FBeVRXUWpReGZIRk1DOEkvdlZLTWtuagpsN2laVnptZFFOYzMrSjl2L01xaDIyemIzUGM1NzBmNmVBT3dubzNVeDFTQk8rUlgyTEZuMzAyMXZmdEFpZi9SCkNDZVI4cWhZRDdVTEhaeE9oekpCeS9tTFlWcUF3aWc4aDBWVnA3eG8yZkViOUJVcjdxanhObHNDZ1lFQTB5NEoKUDI0UzFrQS81OFFtWlZSaVVnYkpoYXZkY3lVbzJqTUd6K0g4NS9sZjhadis2ODczczFRWHp3cFA5UTE3V2ZuRQpsQ0ZTNTMyYnJUa1BWQXV5cjZJQnhySEZlTm8wVFV1emZKcVhyU21DNnRONXRud2VpMmd0NnA4NHZ5elgyem8wCkdvamlCN0Z1MHV2ckFuVkl5V3Y1NjFhTDhTMkpHZ251bGIvdEZMY0NnWUVBblU2bEV3RFI4dXhidnF4L0NPdEEKTzVSUyt3NERSTjhiZWtPWExDTmw0Y3ZVSTlCVGhXOUlDcVNEblNCa29STkV0dlNPb25FSTgxUnFlZnI0ZzRtUgpyYlU5NkJBdldQbnpSUUpYTlh3ZTdEbkhzczhVKzUvbGx0NkhyMnFTM2Y4K0hrNFhzdlc3MlhBV2xLSE0zditoCjVLV3JUTEcwRGtZT1ovWU9OWklXSkNVQ2dZRUFoOFpaZThheHBiNjVWN0h0WGNqYlRoRy9ENHBvWHRmTlpEemEKYkYzSDFQMm9paGJaMTVMQi96SlovR3VNN09rMkhhdFFrRXk4bU1sUmhoWGhuUS9JV3RPMmdLRmJiVkFkd0hTagplY0dMTTNJMUVmWFZrMVpKN1NlV0hmc3c2UGxUNzBlRXFHckpxRmlVQUZoZVNMNVBoZkUrUmVqbklMRStMTmVsCjVmSDM1QkVDZ1lFQWd1dEJtUGxrWWYvMkovYlFaNmg4WnlLd25GYytUQy9IZGlVbzhFNjlCdXNiOGxQZXlYencKTHdlQXh2WThkNkNWeC9RTGZPV3h2NVJrQ29vR2xNV2orbmJOWFY3VnZ3V1lGVkdSczM2cldRMnpqZCsxZ1BkOApTQjlUZVNFK0lvU0QwNjRHU01uNml3cGZUWm11NEhCUDZhbTJYSkdaNWpNTXZReVVvMzhDNTFrPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= diff --git a/.internal-dns-e2e/kubernetes/project-b-bootstrap.json b/.internal-dns-e2e/kubernetes/project-b-bootstrap.json new file mode 100644 index 00000000..f3cd22b3 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-b-bootstrap.json @@ -0,0 +1,946 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "project-e2e" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "compute-system" + } + }, + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": "network-system" + } + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "v1", + "automountServiceAccountToken": false, + "kind": "ServiceAccount", + "metadata": { + "name": "internal-dns-fleet", + "namespace": "internal-dns-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-vpc-integration", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status", + "dnsmanagednamespaces", + "dnsmanagednamespaces/status" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-product-writer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations", + "dnsrecordcontributions" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants", + "dnsresolvercontexts", + "dnsmanagednamespaces", + "dnsnamingpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsrecordcontributions/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-customer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsnamingpolicies", + "dnsregistrations" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolvercontexts", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-grant-issuer", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsregistrations" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnscontributiongrants" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "internal-dns-control-plane" + }, + "rules": [ + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnsrecordsets", + "dnszoneassociations", + "dnsregistrations", + "dnscontributiongrants", + "dnsrecordcontributions", + "dnsnamingpolicies", + "dnsresolvercontexts", + "dnsresolveraccessbindings" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnszones", + "dnszoneassociations" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings", + "dnspublicationmanifests", + "dnspublicationchunks", + "dnstransportoutboxes", + "dnspublicationownerships", + "dnsmanagednamespaces" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "dns.networking.miloapis.com" + ], + "resources": [ + "dnsresolverbindings/status", + "dnspublicationmanifests/status", + "dnstransportoutboxes/status", + "dnspublicationownerships/status", + "dnsmanagednamespaces/status", + "dnszones/status", + "dnszoneassociations/status", + "dnsnamingpolicies/status", + "dnsregistrations/status", + "dnscontributiongrants/status", + "dnsrecordcontributions/status", + "dnsresolvercontexts/status", + "dnsresolveraccessbindings/status" + ], + "verbs": [ + "get", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "internal-dns-source-worker", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "internal-dns-control-plane" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-customer", + "namespace": "project-e2e" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-customer", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-customer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-customer-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-b" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-customer-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-customer-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-customer-self", + "namespace": "project-e2e" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-customer" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-customer-self", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-customer-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-customer", + "namespace": "project-e2e" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-publisher", + "namespace": "compute-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-publisher", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-product-writer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-publisher-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-b" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-publisher-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-publisher-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-publisher-self", + "namespace": "compute-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-publisher" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-publisher-self", + "namespace": "compute-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-publisher-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-publisher", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-grant-issuer", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-grant-issuer" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-grant-issuer-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-b" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-grant-issuer-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-grant-issuer-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-grant-issuer-self", + "namespace": "compute-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-grant-issuer" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-grant-issuer-self", + "namespace": "compute-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-grant-issuer-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-grant-issuer", + "namespace": "compute-system" + } + ] + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "dns-vpc-integration", + "namespace": "network-system" + } + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-vpc-integration", + "namespace": "project-e2e" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-vpc-integration" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "dns-vpc-integration-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-b" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "dns-vpc-integration-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "dns-vpc-integration-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "dns-vpc-integration-self", + "namespace": "network-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "dns-vpc-integration" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "dns-vpc-integration-self", + "namespace": "network-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "dns-vpc-integration-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "dns-vpc-integration", + "namespace": "network-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "internal-dns-control-plane-parent" + }, + "rules": [ + { + "apiGroups": [ + "authentication.k8s.io" + ], + "resourceNames": [ + "project-b" + ], + "resources": [ + "userextras/iam.miloapis.com/parent-name" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "internal-dns-control-plane-parent" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "internal-dns-control-plane-parent" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "internal-dns-control-plane-self", + "namespace": "internal-dns-system" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "internal-dns-control-plane" + ], + "resources": [ + "serviceaccounts" + ], + "verbs": [ + "impersonate" + ] + } + ] + }, + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + "metadata": { + "name": "internal-dns-control-plane-self", + "namespace": "internal-dns-system" + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "internal-dns-control-plane-self" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "internal-dns-control-plane", + "namespace": "internal-dns-system" + } + ] + }, + { + "apiVersion": "dns.networking.miloapis.com/v1alpha1", + "kind": "DNSZoneClass", + "metadata": { + "labels": { + "internal-dns.datum.net/qualification": "true" + }, + "name": "private-bind" + }, + "spec": { + "controllerName": "bind", + "nameServerPolicy": { + "mode": "Static", + "static": { + "servers": [ + "ns.internal." + ] + } + } + } + } + ], + "kind": "List" +} diff --git a/.internal-dns-e2e/kubernetes/project-b-internal.kubeconfig b/.internal-dns-e2e/kubernetes/project-b-internal.kubeconfig new file mode 100644 index 00000000..11f2efe0 --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-b-internal.kubeconfig @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUWNCS2pycFlIZ1F3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalU1TVRaYUZ3MHpOakV3TURreU16QTBNVFphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURpTW8zeWwwY1BDTU1vUUsyMzVMdmhXaTRLZGJoTHJITVR6RzIyNGtOL2R1eVgxeTBsRnlGdDFtOXcKTG1qa2N6YVptR21ERnZXV0ZPRUEwdlBoTmw0OENacERaKzBvUzVGYTVlNCs0dmNPa0tOdHBYOVVWQ0V6TkJhSgptRzZQN0cwVTI2QmlkUWEyVkN6SGVvMjZUam9Uczd3cFduT2NpY2RydlV3Z2F3OFVOSmRSQzl0ek8rUytlV1YvCkFVSk4wbmd3dEordjg0TlV4THhaWitkajUyK1BydlQwN2tPQmJNd2gzRGhFVElBYW9hSHgzaUQwRStzUk9JbWEKblU1RHJlNnJZRW9VL1hOWXd3dVdoVTZYR1FGWDR3ZVN1WGZVTU5jelloUWhJaFZyeWMwUGV6MEtvTjR5amZqTApQZjR6SGJIRTYzT2l2a0UwYzNJN1VuYjl5UzNyQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRRWZIdE5TRlJOU24wMDQ2S3JkTC8yc2NWSm5qQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVRXcVA3WUxKMwpWWkl2aEhiZGRNdDQvak9pMUt2bkx2b3NSSVVqLzZoNkZoMzZVNHFrVkZOdFF3eVM4K2JnMTBDbmpnMkcwZml5CjllQnNSV1RDYVFTNnBmekZScXA0U3gvQURFaDJsWGNkeFVuNTNwWHk5NE4xTmQxTnh0S2IzbVNBdGRJREpraHIKNE5kVEFwSFRxN3lrMjJzT2pFc0NlSzBPQ2UrWU9BTktCZVczNjhyemNjek45SFVKM1RNczRLdUo2Q2dVeTJiego4YzBzcjl2T1BlNzFCWDBRUFJwNk5ZUXkzcFp4U3NCK3ppeWp3eDhrYkRxM2IzVmYyNXNaUlhkZkpLbG1OZXRJClNVN3g3aGlBZ0FWQnJ5Vk55MVBPSjdMbXZGYnNIdHNNTnF2WlVreVhBQW1TSVM0SExLSFQyR3hLRHFlWmlVd0kKUjVkMnFLNEk4OVhoCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://dns-edge-control-plane:6443 + name: kind-dns-edge +contexts: +- context: + cluster: kind-dns-edge + user: kind-dns-edge + name: kind-dns-edge +current-context: kind-dns-edge +kind: Config +users: +- name: kind-dns-edge + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJS2VUdmVtajlIRjh3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalU1TVRaYUZ3MHlOekV3TVRJeU16QTBNVFphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDMjRKUHgKY2w0WklmUnRxK0U3WWwvbTRZY1VMSDhtMHY2UEF6UkNVdis0WVZiTzZRcE1IbGJLd2d2SkczaFNsVDZNaHBaQgozV1FhWGFHQUtzRkxPR0NGaDdPL2tTb1FtLzBrdHlEQkE3dTAvdE84OGFsak9kZjI0VDhIeThZZ1R4bGdHbmE1ClFFa0pUK3JnSHh4cklqK3dVc3ZNRDJEVDdZcUttZlprNDUxUmFyRmFqT2RNbnBFWUk1TCtwRDk0VjFBZDFaUWwKcVFFNXV3bTd0aUFIYXVodkNMK3VBUGgyYmF1a1dEL01VM3dZMWhJRlZDeGlXejBSQXhnYnJvWTBJRU5wR2YxKwpIei9lNHloY1hsT0xkdDl4OTZQVzdwYUg2K28vNW53UWoxcktQanZmSGl6bGtmRzZuMVl5azdtWmZqbnZvdERtCnB2MTJvbHpVWVhibU82WUpBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkFSOGUwMUlWRTFLZlRUagpvcXQwdi9heHhVbWVNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJBSGVFN1k5S3ZGTVQ3bTdNa28xcTdvUTFJCmM0TFJrS0Fmcm40UEZmR2V0NU85dFdDYVkwRlNJRXNNL2pMMkN4VXpJU1kzZjk3SE9xV2MrSEdYMUFiWitnMzMKSEVJTUU1QzJKaTVlbVRtblVwaHVnUkFuWlVTQ2lybkNxaWNlY0ZMR1FNQ2dBVWUwQVo3MU1WQWpkcDRvYzZ2YQo3R0xBSERudWVXSCs5MExuUXFCdnlEbjNTeUx3dDl3M1lFa3VEUkxXNlhmVGZxZW91bXZmT2E5Y1dkNW1XT3ZrCmhUQW43Y1lKMlZYdU9Vdzl6cUZUNWpXdEphOThrbEF3VWNoU09lMnBoa1FhVGlQbys2eVI2UWFhUGJuVGlkUDcKK3VSVVlrbUVZc1dSZmoxVEFOMkhWR05XdWhGTGtIcktBV0pEaVphckNsc1p4OXBrTkd3Z2tndVlvQXRYCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdHVDVDhYSmVHU0gwYmF2aE8ySmY1dUdIRkN4L0p0TCtqd00wUWxML3VHRld6dWtLClRCNVd5c0lMeVJ0NFVwVStqSWFXUWQxa0dsMmhnQ3JCU3poZ2hZZXp2NUVxRUp2OUpMY2d3UU83dFA3VHZQR3AKWXpuWDl1RS9COHZHSUU4WllCcDJ1VUJKQ1UvcTRCOGNheUkvc0ZMTHpBOWcwKzJLaXBuMlpPT2RVV3F4V296bgpUSjZSR0NPUy9xUS9lRmRRSGRXVUpha0JPYnNKdTdZZ0Iycm9id2kvcmdENGRtMnJwRmcvekZOOEdOWVNCVlFzCllsczlFUU1ZRzY2R05DQkRhUm45Zmg4LzN1TW9YRjVUaTNiZmNmZWoxdTZXaCt2cVArWjhFSTlheWo0NzN4NHMKNVpIeHVwOVdNcE81bVg0NTc2TFE1cWI5ZHFKYzFHRjI1anVtQ1FJREFRQUJBb0lCQUU1OCtaSDlhMnh1K2NpZQpKZGRHWS9Ha0pzVGVTaDVBSFZnRTRJZDh0dHNNRCtWUytJUHQyNE0vSUM3S2x1d2V1TTg0TGtJTGdseWNoZVdMCnRCVVVGNzBIbm5wZ0U4VTRKYi85eHRXQVJDNHMxblIzNWFLOEh6Z0t1WElmbjZPYlAwRlRhbFFjeHB2MERGTHYKbDFOVnlpN2pnaXROQ0ZvOHo2S243S3VsVXc0dk9oWFBjK2lPR2svODdNNlV6Mm1lais1VWhuQkhKWXNuQlI4MQpPR1l6WE9QeHpYQzdFSGxpZ3VIOU0rQ1FONTZ5NFFkTDZSZWQycDdlMW4vS3QwajdzdHJMeWMvUC9hZmdXaWpICnVkQW9oaEhuZENZL2w5bHlGVnpPYktDWmtLeWJyTXZteStWWVI4QzlyMCs2RXBlaTBlV1k2blZCSHAxRWhLOWMKYjE0K2pUY0NnWUVBejRKRnQzRVBMaTgrUmpZdTdzZWVEUGZCNVBkeWJ4Uk5RbE51OWRCcHlJS0NLWmM2VU1TQwp4eVRoWGQwaVNxbzNxK3RGMVR4Sk8rNTRsVllITS9pVjJ5a0VSZVAwSzN0TU1XRWxqdjBLTTZvZkMyZ3g4SWhUCnFyTVlTemUwUkNDV0txcFZoRmFDajFYTVUzWjNzb3I2ajhrZHBYbitmc21qbWg3ZnN1eEFUZ2NDZ1lFQTRaekcKK1MwaHBJb0o0b3JNdDFQZDRzd3Y2MEsvN2xobzZXZFJ1TGlRcjEwc0V1TWljT0hYVngzV1dUY05MNjBodm1wdQpYLy9sR3VGNjRET1M3dkJieTdxVVZ2RzlSRThsMmlJWmQ1bXozN1J5T2hBT3ArUWk2UGI0aHRHZVVnakNNR3MxCm5RRFlKL1ZCVWIvZzV1R2RBSy85V1VhYjZLVUF2c0JCSzdRaloyOENnWUJhZVRRb1hSVC9ULzNZNHZybmhRVTIKSmpmVnVzV2NCTUFDc1JucDl5cUlEODdzajFFUHNmNUxFWnRwTDJjcUlya1BMZzlPZjNCMnZ1eTRQNy80T3VRcgpuK0NMRUc1L0hzMERvcFdibUZ3QVZxWWVkUm1CQkE5bWw1dDZBQm1Lc3FLR2k2bXBmNGFCTkNsWUlvamh3WktwCmFraVF3M1hnbGMzTS9yVU43OC9CdFFLQmdGUVErd00yRWVjaG1IVlZpdHAwNGFUMHVKWk5GUDlXNmlDdVl3RVYKcExQelg4elozYWQ2UDZjeWN3QmpSd0tRM0pYN1NvMXk2SWVQcFIyTm1oNTJhWDRNVStIaHljaCtSWGgyWkU3dwpjdG5VZnd1cm1BcHZNNkxqQTlBWW80bWx3ZFlRM0xWWDd6TlVhN1p1YlBzYTdrbW5TaHZOOTZKRWVYYWFaSzVJClBUajVBb0dCQUxERnRpWHFXMElIVnA3TWdpaFRGQm8wS2FraUhLQ25tdlluWkNRWStrZ3VZY21kR3RFWmh4K0EKTi9LdG54RDA2WGRteCszOW5aRDdNaU5DZlYzWS9IamhqV2laVjlpeUJ4WFdidjVwRTd0V0xKaEJvdmdJUU5aMgp4bnFlTk5ybmlCVVFZQ2owTHg5SHhiOHg2SmpmTlREZTNxbGg0YlZKYXllNGkxUmlYMXdyCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + diff --git a/.internal-dns-e2e/kubernetes/project-b.kubeconfig b/.internal-dns-e2e/kubernetes/project-b.kubeconfig new file mode 100644 index 00000000..2ea48d3e --- /dev/null +++ b/.internal-dns-e2e/kubernetes/project-b.kubeconfig @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUWNCS2pycFlIZ1F3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalU1TVRaYUZ3MHpOakV3TURreU16QTBNVFphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURpTW8zeWwwY1BDTU1vUUsyMzVMdmhXaTRLZGJoTHJITVR6RzIyNGtOL2R1eVgxeTBsRnlGdDFtOXcKTG1qa2N6YVptR21ERnZXV0ZPRUEwdlBoTmw0OENacERaKzBvUzVGYTVlNCs0dmNPa0tOdHBYOVVWQ0V6TkJhSgptRzZQN0cwVTI2QmlkUWEyVkN6SGVvMjZUam9Uczd3cFduT2NpY2RydlV3Z2F3OFVOSmRSQzl0ek8rUytlV1YvCkFVSk4wbmd3dEordjg0TlV4THhaWitkajUyK1BydlQwN2tPQmJNd2gzRGhFVElBYW9hSHgzaUQwRStzUk9JbWEKblU1RHJlNnJZRW9VL1hOWXd3dVdoVTZYR1FGWDR3ZVN1WGZVTU5jelloUWhJaFZyeWMwUGV6MEtvTjR5amZqTApQZjR6SGJIRTYzT2l2a0UwYzNJN1VuYjl5UzNyQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRRWZIdE5TRlJOU24wMDQ2S3JkTC8yc2NWSm5qQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVRXcVA3WUxKMwpWWkl2aEhiZGRNdDQvak9pMUt2bkx2b3NSSVVqLzZoNkZoMzZVNHFrVkZOdFF3eVM4K2JnMTBDbmpnMkcwZml5CjllQnNSV1RDYVFTNnBmekZScXA0U3gvQURFaDJsWGNkeFVuNTNwWHk5NE4xTmQxTnh0S2IzbVNBdGRJREpraHIKNE5kVEFwSFRxN3lrMjJzT2pFc0NlSzBPQ2UrWU9BTktCZVczNjhyemNjek45SFVKM1RNczRLdUo2Q2dVeTJiego4YzBzcjl2T1BlNzFCWDBRUFJwNk5ZUXkzcFp4U3NCK3ppeWp3eDhrYkRxM2IzVmYyNXNaUlhkZkpLbG1OZXRJClNVN3g3aGlBZ0FWQnJ5Vk55MVBPSjdMbXZGYnNIdHNNTnF2WlVreVhBQW1TSVM0SExLSFQyR3hLRHFlWmlVd0kKUjVkMnFLNEk4OVhoCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:63980 + name: kind-dns-edge +contexts: +- context: + cluster: kind-dns-edge + user: kind-dns-edge + name: kind-dns-edge +current-context: kind-dns-edge +kind: Config +users: +- name: kind-dns-edge + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJS2VUdmVtajlIRjh3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpFd01USXlNalU1TVRaYUZ3MHlOekV3TVRJeU16QTBNVFphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDMjRKUHgKY2w0WklmUnRxK0U3WWwvbTRZY1VMSDhtMHY2UEF6UkNVdis0WVZiTzZRcE1IbGJLd2d2SkczaFNsVDZNaHBaQgozV1FhWGFHQUtzRkxPR0NGaDdPL2tTb1FtLzBrdHlEQkE3dTAvdE84OGFsak9kZjI0VDhIeThZZ1R4bGdHbmE1ClFFa0pUK3JnSHh4cklqK3dVc3ZNRDJEVDdZcUttZlprNDUxUmFyRmFqT2RNbnBFWUk1TCtwRDk0VjFBZDFaUWwKcVFFNXV3bTd0aUFIYXVodkNMK3VBUGgyYmF1a1dEL01VM3dZMWhJRlZDeGlXejBSQXhnYnJvWTBJRU5wR2YxKwpIei9lNHloY1hsT0xkdDl4OTZQVzdwYUg2K28vNW53UWoxcktQanZmSGl6bGtmRzZuMVl5azdtWmZqbnZvdERtCnB2MTJvbHpVWVhibU82WUpBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkFSOGUwMUlWRTFLZlRUagpvcXQwdi9heHhVbWVNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJBSGVFN1k5S3ZGTVQ3bTdNa28xcTdvUTFJCmM0TFJrS0Fmcm40UEZmR2V0NU85dFdDYVkwRlNJRXNNL2pMMkN4VXpJU1kzZjk3SE9xV2MrSEdYMUFiWitnMzMKSEVJTUU1QzJKaTVlbVRtblVwaHVnUkFuWlVTQ2lybkNxaWNlY0ZMR1FNQ2dBVWUwQVo3MU1WQWpkcDRvYzZ2YQo3R0xBSERudWVXSCs5MExuUXFCdnlEbjNTeUx3dDl3M1lFa3VEUkxXNlhmVGZxZW91bXZmT2E5Y1dkNW1XT3ZrCmhUQW43Y1lKMlZYdU9Vdzl6cUZUNWpXdEphOThrbEF3VWNoU09lMnBoa1FhVGlQbys2eVI2UWFhUGJuVGlkUDcKK3VSVVlrbUVZc1dSZmoxVEFOMkhWR05XdWhGTGtIcktBV0pEaVphckNsc1p4OXBrTkd3Z2tndVlvQXRYCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdHVDVDhYSmVHU0gwYmF2aE8ySmY1dUdIRkN4L0p0TCtqd00wUWxML3VHRld6dWtLClRCNVd5c0lMeVJ0NFVwVStqSWFXUWQxa0dsMmhnQ3JCU3poZ2hZZXp2NUVxRUp2OUpMY2d3UU83dFA3VHZQR3AKWXpuWDl1RS9COHZHSUU4WllCcDJ1VUJKQ1UvcTRCOGNheUkvc0ZMTHpBOWcwKzJLaXBuMlpPT2RVV3F4V296bgpUSjZSR0NPUy9xUS9lRmRRSGRXVUpha0JPYnNKdTdZZ0Iycm9id2kvcmdENGRtMnJwRmcvekZOOEdOWVNCVlFzCllsczlFUU1ZRzY2R05DQkRhUm45Zmg4LzN1TW9YRjVUaTNiZmNmZWoxdTZXaCt2cVArWjhFSTlheWo0NzN4NHMKNVpIeHVwOVdNcE81bVg0NTc2TFE1cWI5ZHFKYzFHRjI1anVtQ1FJREFRQUJBb0lCQUU1OCtaSDlhMnh1K2NpZQpKZGRHWS9Ha0pzVGVTaDVBSFZnRTRJZDh0dHNNRCtWUytJUHQyNE0vSUM3S2x1d2V1TTg0TGtJTGdseWNoZVdMCnRCVVVGNzBIbm5wZ0U4VTRKYi85eHRXQVJDNHMxblIzNWFLOEh6Z0t1WElmbjZPYlAwRlRhbFFjeHB2MERGTHYKbDFOVnlpN2pnaXROQ0ZvOHo2S243S3VsVXc0dk9oWFBjK2lPR2svODdNNlV6Mm1lais1VWhuQkhKWXNuQlI4MQpPR1l6WE9QeHpYQzdFSGxpZ3VIOU0rQ1FONTZ5NFFkTDZSZWQycDdlMW4vS3QwajdzdHJMeWMvUC9hZmdXaWpICnVkQW9oaEhuZENZL2w5bHlGVnpPYktDWmtLeWJyTXZteStWWVI4QzlyMCs2RXBlaTBlV1k2blZCSHAxRWhLOWMKYjE0K2pUY0NnWUVBejRKRnQzRVBMaTgrUmpZdTdzZWVEUGZCNVBkeWJ4Uk5RbE51OWRCcHlJS0NLWmM2VU1TQwp4eVRoWGQwaVNxbzNxK3RGMVR4Sk8rNTRsVllITS9pVjJ5a0VSZVAwSzN0TU1XRWxqdjBLTTZvZkMyZ3g4SWhUCnFyTVlTemUwUkNDV0txcFZoRmFDajFYTVUzWjNzb3I2ajhrZHBYbitmc21qbWg3ZnN1eEFUZ2NDZ1lFQTRaekcKK1MwaHBJb0o0b3JNdDFQZDRzd3Y2MEsvN2xobzZXZFJ1TGlRcjEwc0V1TWljT0hYVngzV1dUY05MNjBodm1wdQpYLy9sR3VGNjRET1M3dkJieTdxVVZ2RzlSRThsMmlJWmQ1bXozN1J5T2hBT3ArUWk2UGI0aHRHZVVnakNNR3MxCm5RRFlKL1ZCVWIvZzV1R2RBSy85V1VhYjZLVUF2c0JCSzdRaloyOENnWUJhZVRRb1hSVC9ULzNZNHZybmhRVTIKSmpmVnVzV2NCTUFDc1JucDl5cUlEODdzajFFUHNmNUxFWnRwTDJjcUlya1BMZzlPZjNCMnZ1eTRQNy80T3VRcgpuK0NMRUc1L0hzMERvcFdibUZ3QVZxWWVkUm1CQkE5bWw1dDZBQm1Lc3FLR2k2bXBmNGFCTkNsWUlvamh3WktwCmFraVF3M1hnbGMzTS9yVU43OC9CdFFLQmdGUVErd00yRWVjaG1IVlZpdHAwNGFUMHVKWk5GUDlXNmlDdVl3RVYKcExQelg4elozYWQ2UDZjeWN3QmpSd0tRM0pYN1NvMXk2SWVQcFIyTm1oNTJhWDRNVStIaHljaCtSWGgyWkU3dwpjdG5VZnd1cm1BcHZNNkxqQTlBWW80bWx3ZFlRM0xWWDd6TlVhN1p1YlBzYTdrbW5TaHZOOTZKRWVYYWFaSzVJClBUajVBb0dCQUxERnRpWHFXMElIVnA3TWdpaFRGQm8wS2FraUhLQ25tdlluWkNRWStrZ3VZY21kR3RFWmh4K0EKTi9LdG54RDA2WGRteCszOW5aRDdNaU5DZlYzWS9IamhqV2laVjlpeUJ4WFdidjVwRTd0V0xKaEJvdmdJUU5aMgp4bnFlTk5ybmlCVVFZQ2owTHg5SHhiOHg2SmpmTlREZTNxbGg0YlZKYXllNGkxUmlYMXdyCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 60f4f726..416bf6ed 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -158,25 +158,6 @@ func TestRegistrationRequiresCurrentZone(t *testing.T) { } } -func TestPublicationStatusDoesNotChangePublisherObservation(t *testing.T) { - h := Handler{Client: fake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()} - old := &dnsv1alpha1.DNSRecordContribution{Status: dnsv1alpha1.DNSRecordContributionStatus{Sequence: 10, Eligible: true}} - current := old.DeepCopy() - current.Status.PublishedRevision = 12 - current.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue}} - req := request("dnsrecordcontributions", "rbac-authorized-controller", current, old) - req.SubResource = "status" - if result := h.Handle(context.Background(), req); !result.Allowed { - t.Fatalf("publication result needs no producer grant: %v", result.Result) - } - current.Status.Sequence++ - req = request("dnsrecordcontributions", "rbac-authorized-controller", current, old) - req.SubResource = "status" - if h.Handle(context.Background(), req).Allowed { - t.Fatal("publication update changed the producer observation without a grant") - } -} - func TestContributionValidatesReferencesForRBACAuthorizedCallers(t *testing.T) { scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { From 8caab44b166f6862f188b0d3273864149a8edf45 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:57:59 -0500 Subject: [PATCH 20/28] docs: Describe DNS mapping validation --- internal/internaldns/admission/handler.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index f2f64ee9..bc191f32 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -1,8 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only -// Package admission validates live DNS references and publisher grant relationships. -// Compiler validation remains necessary, but cannot infer a Kubernetes caller -// from a stored object's spec. Install this webhook with failurePolicy: Fail. +// Package admission validates live references between private DNS resources. package admission import ( @@ -97,7 +95,7 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube return fmt.Errorf("resolver context lifetime is not current") } if resolverContext.Status.AccessEpoch < 1 || current.Spec.Version.Epoch != resolverContext.Status.AccessEpoch { - return fmt.Errorf("access writer epoch is retired or not issued") + return fmt.Errorf("access version does not match the current context epoch") } ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { From 76e7e93859cbb2ba6f2173e5036623a0f58fd67e Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 13:00:48 -0500 Subject: [PATCH 21/28] docs: Describe DNS mappings and reference validation From 7e8770d6febf690f758498216655c376391c4ae1 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 13:44:21 -0500 Subject: [PATCH 22/28] feat: Simplify private DNS publication and access --- api/v1alpha1/internaldns_project_types.go | 101 ++------- .../internaldns_validation_envtest_test.go | 63 +++--- api/v1alpha1/zz_generated.deepcopy.go | 123 +---------- ...ing.miloapis.com_dnsmanagednamespaces.yaml | 191 ------------------ ...g.miloapis.com_dnsrecordcontributions.yaml | 22 +- ...working.miloapis.com_dnsregistrations.yaml | 6 - ...iloapis.com_dnsresolveraccessbindings.yaml | 64 +----- ...king.miloapis.com_dnsresolvercontexts.yaml | 3 - config/crd/kustomization.yaml | 3 +- internal/internaldns/admission/handler.go | 3 - .../internaldns/admission/handler_test.go | 5 +- internal/internaldns/model/model.go | 6 +- 12 files changed, 71 insertions(+), 519 deletions(-) delete mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 6cb1cbd0..bd49de5b 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -56,7 +56,6 @@ type DNSResolverContextManagedNamespaceStatus struct { type DNSResolverContextStatus struct { ManagedNamespace DNSResolverContextManagedNamespaceStatus `json:"managedNamespace,omitempty"` - AccessEpoch int64 `json:"accessEpoch,omitempty"` ServingTargets []DNSResolverContextServingTarget `json:"servingTargets,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -83,18 +82,6 @@ type DNSResolverContextList struct { Items []DNSResolverContext `json:"items"` } -// +kubebuilder:validation:Enum=DestinationAddress -type DNSResolverQueryIdentityType string - -const DNSResolverQueryIdentityDestinationAddress DNSResolverQueryIdentityType = "DestinationAddress" - -type DNSResolverQueryIdentity struct { - Type DNSResolverQueryIdentityType `json:"type"` - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - Value string `json:"value"` -} - type DNSResolverAccessVersion struct { // +kubebuilder:validation:Minimum=1 Epoch int64 `json:"epoch"` @@ -104,29 +91,22 @@ type DNSResolverAccessVersion struct { // +kubebuilder:validation:XValidation:rule="self.contextRef.name.size() > 0 && has(self.contextRef.uid) && self.contextRef.uid.size() > 0",message="contextRef requires name and UID" // +kubebuilder:validation:XValidation:rule="self.contextRef == oldSelf.contextRef",message="contextRef is immutable" -// +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity && self.port == oldSelf.port && self.transports == oldSelf.transports",message="access identity and transport are immutable" -// +kubebuilder:validation:XValidation:rule="self.version.epoch == oldSelf.version.epoch",message="epoch is immutable for an access binding" -// +kubebuilder:validation:XValidation:rule="self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence",message="version changes require an increasing sequence" +// +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.destinationAddress == oldSelf.destinationAddress",message="access destination is immutable" type DNSResolverAccessBindingSpec struct { ContextRef DNSObjectReference `json:"contextRef"` // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 - Region string `json:"region"` - QueryIdentity DNSResolverQueryIdentity `json:"queryIdentity"` - // +kubebuilder:validation:Enum=53 - Port int32 `json:"port"` - // +kubebuilder:validation:MinItems=2 - // +kubebuilder:validation:MaxItems=2 - // +kubebuilder:validation:items:Enum=UDP;TCP - // +listType=set - Transports []string `json:"transports"` - Version DNSResolverAccessVersion `json:"version"` + Region string `json:"region"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="destinationAddress must be an IP address" + DestinationAddress string `json:"destinationAddress"` } type DNSResolverAccessBindingStatus struct { - ObservedSequence int64 `json:"observedSequence,omitempty"` - BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedSequence,omitempty"` + BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -163,22 +143,13 @@ type DNSZoneAssociationList struct { Items []DNSZoneAssociation `json:"items"` } -// +kubebuilder:validation:Enum=EligibleContributions;Persistent -type DNSPublicationPolicy string - -const ( - DNSPublicationPolicyEligibleContributions DNSPublicationPolicy = "EligibleContributions" - DNSPublicationPolicyPersistent DNSPublicationPolicy = "Persistent" -) - const DNSRegistrationMaxTTLSeconds int32 = 30 // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSRegistrationSpec struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - Name string `json:"name"` - RecordTypes []RRType `json:"recordTypes"` - PublicationPolicy DNSPublicationPolicy `json:"publicationPolicy"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + Name string `json:"name"` + RecordTypes []RRType `json:"recordTypes"` // TTLSeconds bounds cached dynamic answers to at most 30 seconds after withdrawal. // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=30 @@ -221,19 +192,14 @@ type DNSContributionRecordSet struct { type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` RecordSets []DNSContributionRecordSet `json:"recordSets"` + Eligible bool `json:"eligible"` + Reason string `json:"reason,omitempty"` } -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.sequence) || (has(self.sequence) && self.sequence >= oldSelf.sequence)",message="observation sequence must not decrease" -// +kubebuilder:validation:XValidation:rule="(has(self.eligible) && self.eligible) == (has(oldSelf.eligible) && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) || self.sequence > oldSelf.sequence))",message="eligibility changes require a newer observation sequence" type DNSRecordContributionStatus struct { - // +kubebuilder:validation:Minimum=1 - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // +kubebuilder:validation:Minimum=1 - Sequence int64 `json:"sequence,omitempty"` - Eligible bool `json:"eligible,omitempty"` - Reason string `json:"reason,omitempty"` - PublishedRevision int64 `json:"publishedRevision,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + PublishedRevision int64 `json:"publishedRevision,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -335,37 +301,6 @@ type DNSNamingPolicyList struct { Items []DNSNamingPolicy `json:"items"` } -// DNSManagedNamespace tracks a resolver context's canonical private namespace. -type DNSManagedNamespaceSpec struct { - ProjectUID types.UID `json:"projectUID"` - ResolverContextRef DNSObjectReference `json:"resolverContextRef"` - DomainSuffix string `json:"domainSuffix"` - DNSZoneClassName string `json:"dnsZoneClassName"` -} - -type DNSManagedNamespaceStatus struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` - AssociationRef DNSObjectReference `json:"associationRef,omitempty"` - CanonicalSuffix string `json:"canonicalSuffix,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -type DNSManagedNamespace struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DNSManagedNamespaceSpec `json:"spec"` - Status DNSManagedNamespaceStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true -type DNSManagedNamespaceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []DNSManagedNamespace `json:"items"` -} - func init() { - SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}, &DNSManagedNamespace{}, &DNSManagedNamespaceList{}) + SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}) } diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index fdcc64d0..99512988 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -55,9 +55,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { } } newAccess := func(name string) *DNSResolverAccessBinding { - return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ - ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: DNSResolverAccessVersion{Epoch: 1, Sequence: 1}, - }} + return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", DestinationAddress: "fd53::1"}} } for _, tc := range []struct { name string @@ -66,14 +64,8 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { {"empty-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "" }}, {"missing-context-uid", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.UID = "" }}, {"empty-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "" }}, - {"empty-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "" }}, - {"unsupported-identity", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Type = "SourceAddress" }}, - {"wrong-port", func(a *DNSResolverAccessBinding) { a.Spec.Port = 5353 }}, - {"missing-tcp", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP"} }}, - {"duplicate-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "UDP"} }}, - {"unsupported-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "TLS"} }}, - {"zero-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch = 0 }}, - {"zero-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 0 }}, + {"empty-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "" }}, + {"invalid-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "resolver.internal" }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name); tc.mutate(a); requireInvalid(t, cl.Create(ctx, a)) }) } @@ -83,13 +75,10 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { }{ {"change-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "other" }}, {"change-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "east" }}, - {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "fd53::2" }}, - {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch++ }}, - {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 1 }}, + {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "fd53::2" }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name) - a.Spec.Version.Sequence = 2 if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } @@ -97,18 +86,17 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { requireInvalid(t, cl.Update(ctx, a)) }) } - t.Run("sequence-and-status", func(t *testing.T) { + t.Run("metadata-and-status", func(t *testing.T) { a := newAccess("renewal") if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Spec.Version.Sequence++ if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } - a.Status.ObservedSequence = a.Spec.Version.Sequence + a.Status.ObservedGeneration = a.Generation if err := cl.Status().Update(ctx, a); err != nil { - t.Fatalf("unchanged access version blocked status update: %v", err) + t.Fatalf("access status update failed: %v", err) } a.Labels = map[string]string{"test": "metadata"} if err := cl.Update(ctx, a); err != nil { @@ -127,7 +115,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { }) for _, ttl := range []int32{0, 31, 2147483647} { t.Run("ttl", func(t *testing.T) { - a := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, PublicationPolicy: DNSPublicationPolicyEligibleContributions, TTLSeconds: ttl}} + a := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, TTLSeconds: ttl}} requireInvalid(t, cl.Create(ctx, a)) }) } @@ -139,30 +127,37 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { a.Spec.RegistrationRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) - t.Run("contribution-observation-order", func(t *testing.T) { - a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "observations", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}}} + t.Run("atomic-contribution-and-stale-update", func(t *testing.T) { + a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "observations", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}, Eligible: true}} if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Status = DNSRecordContributionStatus{ObservedGeneration: a.Generation, Sequence: 2, Eligible: true} - if err := cl.Status().Update(ctx, a); err != nil { + stale := a.DeepCopy() + generation := a.Generation + a.Spec.Eligible = false + a.Spec.Reason = "EndpointUnhealthy" + a.Spec.RecordSets = []DNSContributionRecordSet{{RecordType: RRTypeA, Records: []RecordEntry{{Name: "web", A: &ARecordSpec{Content: "10.0.0.9"}}}}} + if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } - decreasing := a.DeepCopy() - decreasing.Status.Sequence = 1 - requireInvalid(t, cl.Status().Update(ctx, decreasing)) - unchanged := a.DeepCopy() - unchanged.Status.Eligible = false - requireInvalid(t, cl.Status().Update(ctx, unchanged)) - a.Status.Sequence++ - a.Status.Eligible = false - if err := cl.Status().Update(ctx, a); err != nil { - t.Fatal(err) + if a.Generation != generation+1 { + t.Fatalf("atomic desired state did not advance generation: %d", a.Generation) } + if err := cl.Update(ctx, stale); !apierrors.IsConflict(err) { + t.Fatalf("stale writer was not rejected: %v", err) + } + a.Status.ObservedGeneration = a.Generation a.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue, Reason: "Applied", LastTransitionTime: metav1.Now()}} if err := cl.Status().Update(ctx, a); err != nil { t.Fatal(err) } + var observed DNSRecordContribution + if err := cl.Get(ctx, client.ObjectKeyFromObject(a), &observed); err != nil { + t.Fatal(err) + } + if observed.Spec.Eligible || observed.Spec.RecordSets[0].Records[0].A.Content != "10.0.0.9" { + t.Fatal("status write changed producer desired state") + } }) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 066a3987..df717379 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -130,105 +130,6 @@ func (in *DNSContributionRecordSet) DeepCopy() *DNSContributionRecordSet { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespace) DeepCopyInto(out *DNSManagedNamespace) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespace. -func (in *DNSManagedNamespace) DeepCopy() *DNSManagedNamespace { - if in == nil { - return nil - } - out := new(DNSManagedNamespace) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSManagedNamespace) 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 *DNSManagedNamespaceList) DeepCopyInto(out *DNSManagedNamespaceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DNSManagedNamespace, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceList. -func (in *DNSManagedNamespaceList) DeepCopy() *DNSManagedNamespaceList { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSManagedNamespaceList) 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 *DNSManagedNamespaceSpec) DeepCopyInto(out *DNSManagedNamespaceSpec) { - *out = *in - out.ResolverContextRef = in.ResolverContextRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceSpec. -func (in *DNSManagedNamespaceSpec) DeepCopy() *DNSManagedNamespaceSpec { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespaceStatus) DeepCopyInto(out *DNSManagedNamespaceStatus) { - *out = *in - out.DNSZoneRef = in.DNSZoneRef - out.AssociationRef = in.AssociationRef - 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 DNSManagedNamespaceStatus. -func (in *DNSManagedNamespaceStatus) DeepCopy() *DNSManagedNamespaceStatus { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSNamingPolicy) DeepCopyInto(out *DNSNamingPolicy) { *out = *in @@ -755,7 +656,7 @@ func (in *DNSResolverAccessBinding) DeepCopyInto(out *DNSResolverAccessBinding) *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -813,13 +714,6 @@ func (in *DNSResolverAccessBindingList) DeepCopyObject() runtime.Object { func (in *DNSResolverAccessBindingSpec) DeepCopyInto(out *DNSResolverAccessBindingSpec) { *out = *in out.ContextRef = in.ContextRef - out.QueryIdentity = in.QueryIdentity - if in.Transports != nil { - in, out := &in.Transports, &out.Transports - *out = make([]string, len(*in)) - copy(*out, *in) - } - out.Version = in.Version } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingSpec. @@ -1023,21 +917,6 @@ 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 *DNSResolverQueryIdentity) DeepCopyInto(out *DNSResolverQueryIdentity) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverQueryIdentity. -func (in *DNSResolverQueryIdentity) DeepCopy() *DNSResolverQueryIdentity { - if in == nil { - return nil - } - out := new(DNSResolverQueryIdentity) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSZone) DeepCopyInto(out *DNSZone) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml deleted file mode 100644 index c57ef52a..00000000 --- a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml +++ /dev/null @@ -1,191 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: dnsmanagednamespaces.dns.networking.miloapis.com -spec: - group: dns.networking.miloapis.com - names: - kind: DNSManagedNamespace - listKind: DNSManagedNamespaceList - plural: dnsmanagednamespaces - singular: dnsmanagednamespace - 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: DNSManagedNamespace tracks a resolver context's canonical - private namespace. - properties: - dnsZoneClassName: - type: string - domainSuffix: - type: string - projectUID: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - type: string - resolverContextRef: - description: |- - DNSObjectReference pins a namespaced object by API-assigned UID. Generation - is used where a writer must also be fenced against policy changes. - properties: - generation: - format: int64 - 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: - - dnsZoneClassName - - domainSuffix - - projectUID - - resolverContextRef - type: object - status: - properties: - associationRef: - 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 - canonicalSuffix: - type: string - 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 - dnsZoneRef: - 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 - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 2ef3f510..690a4495 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -38,6 +38,10 @@ spec: type: object spec: properties: + eligible: + type: boolean + reason: + type: string recordSets: items: properties: @@ -304,6 +308,7 @@ spec: - name type: object required: + - eligible - recordSets - registrationRef type: object @@ -372,30 +377,13 @@ spec: - type type: object type: array - eligible: - type: boolean observedGeneration: format: int64 - minimum: 1 type: integer publishedRevision: format: int64 type: integer - reason: - type: string - sequence: - format: int64 - minimum: 1 - type: integer type: object - x-kubernetes-validations: - - message: observation sequence must not decrease - rule: '!has(oldSelf.sequence) || (has(self.sequence) && self.sequence - >= oldSelf.sequence)' - - message: eligibility changes require a newer observation sequence - rule: (has(self.eligible) && self.eligible) == (has(oldSelf.eligible) - && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) - || self.sequence > oldSelf.sequence)) required: - spec type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml index 701b0e8a..2eca271a 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -62,11 +62,6 @@ spec: type: object name: type: string - publicationPolicy: - enum: - - EligibleContributions - - Persistent - type: string recordTypes: items: enum: @@ -100,7 +95,6 @@ spec: required: - dnsZoneRef - name - - publicationPolicy - recordTypes - ttlSeconds type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 45c904b9..2cfebb72 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -60,60 +60,21 @@ spec: required: - name type: object - port: - enum: - - 53 - format: int32 - type: integer - queryIdentity: - properties: - type: - enum: - - DestinationAddress - type: string - value: - maxLength: 253 - minLength: 1 - type: string - required: - - type - - value - type: object + destinationAddress: + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: destinationAddress must be an IP address + rule: isIP(self) region: maxLength: 253 minLength: 1 type: string - transports: - items: - enum: - - UDP - - TCP - type: string - maxItems: 2 - minItems: 2 - type: array - x-kubernetes-list-type: set - version: - properties: - epoch: - format: int64 - minimum: 1 - type: integer - sequence: - format: int64 - minimum: 1 - type: integer - required: - - epoch - - sequence - type: object required: - contextRef - - port - - queryIdentity + - destinationAddress - region - - transports - - version type: object x-kubernetes-validations: - message: contextRef requires name and UID @@ -121,13 +82,8 @@ spec: self.contextRef.uid.size() > 0 - message: contextRef is immutable rule: self.contextRef == oldSelf.contextRef - - message: access identity and transport are immutable - rule: self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity - && self.port == oldSelf.port && self.transports == oldSelf.transports - - message: epoch is immutable for an access binding - rule: self.version.epoch == oldSelf.version.epoch - - message: version changes require an increasing sequence - rule: self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence + - message: access destination is immutable + rule: self.region == oldSelf.region && self.destinationAddress == oldSelf.destinationAddress status: properties: bindingRef: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml index 2bdf3da0..64cc3207 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -58,9 +58,6 @@ spec: rule: oldSelf.consumerID == self.consumerID status: properties: - accessEpoch: - format: int64 - type: integer conditions: items: description: Condition contains details for one aspect of the current diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8d7e5599..81e6435a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,7 +6,8 @@ resources: - bases/dns.networking.miloapis.com_dnszones.yaml - bases/dns.networking.miloapis.com_dnsrecordsets.yaml - bases/dns.networking.miloapis.com_dnszonediscoveries.yaml -- bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml + + - bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml - bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml - bases/dns.networking.miloapis.com_dnsregistrations.yaml diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index bc191f32..e07fa5a6 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -94,9 +94,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if resolverContext.UID != current.Spec.ContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() { return fmt.Errorf("resolver context lifetime is not current") } - if resolverContext.Status.AccessEpoch < 1 || current.Spec.Version.Epoch != resolverContext.Status.AccessEpoch { - return fmt.Errorf("access version does not match the current context epoch") - } ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { return fmt.Errorf("resolver context is not ready for its current generation") diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 416bf6ed..927fb676 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -88,12 +88,11 @@ func TestResolverAccessRequiresCurrentContext(t *testing.T) { if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } - resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessEpoch: 3}} + resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{}} setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build()} - old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: dnsv1alpha1.DNSResolverAccessVersion{Epoch: 3, Sequence: 9}}} + old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", DestinationAddress: "fd70:100::10"}} current := old.DeepCopy() - current.Spec.Version.Sequence = 10 req := request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if res := h.Handle(context.Background(), req); !res.Allowed { t.Fatalf("current integration update denied: %v", res.Result) diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index a7257475..1f613b98 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -17,7 +17,7 @@ import ( "github.com/miekg/dns" ) -const APIVersion = "internal.dns.networking.miloapis.com/v1alpha5" +const APIVersion = "internal.dns.networking.miloapis.com/v1alpha6" type Kind string @@ -349,6 +349,8 @@ type ObservationFence struct { ContributionUID string `json:"contributionUID"` Generation uint64 `json:"generation"` Sequence uint64 `json:"sequence"` + Retired bool `json:"retired,omitempty"` + SourceName string `json:"sourceName,omitempty"` } func (p PublicationPlan) Validate() error { @@ -415,7 +417,7 @@ func (p PublicationPlan) Validate() error { } } else { observation, ok := observationByUID[rec.ContributionUID] - if !ok || rec.Generation != observation.Generation || rec.Sequence != observation.Sequence { + if !ok || observation.Retired || rec.Generation != observation.Generation || rec.Sequence != observation.Sequence { return fmt.Errorf("RRset %q record fence does not match contribution observation %q", key, rec.ContributionUID) } } From e5baa7b0028b728f83da3959ae2f40d6c34b57af Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 21 Oct 2026 19:55:49 -0500 Subject: [PATCH 23/28] fix: allow DNS contribution refresh and cleanup --- api/v1alpha1/internaldns_project_types.go | 4 ++-- api/v1alpha1/internaldns_validation_envtest_test.go | 4 ++++ ...networking.miloapis.com_dnsrecordcontributions.yaml | 5 +++-- ...working.miloapis.com_dnsresolveraccessbindings.yaml | 2 +- internal/internaldns/admission/handler.go | 9 +++++++++ internal/internaldns/admission/handler_test.go | 10 ++++++++++ 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index bd49de5b..3792af66 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -104,7 +104,7 @@ type DNSResolverAccessBindingSpec struct { } type DNSResolverAccessBindingStatus struct { - ObservedGeneration int64 `json:"observedSequence,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -188,7 +188,7 @@ type DNSContributionRecordSet struct { } // +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0",message="registrationRef requires name, UID, and generation" -// +kubebuilder:validation:XValidation:rule="self.registrationRef == oldSelf.registrationRef",message="contribution registration reference is immutable" +// +kubebuilder:validation:XValidation:rule="self.registrationRef.name == oldSelf.registrationRef.name && self.registrationRef.uid == oldSelf.registrationRef.uid",message="contribution registration identity is immutable" type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` RecordSets []DNSContributionRecordSet `json:"recordSets"` diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index 99512988..6fc45c37 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -124,6 +124,10 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } + a.Spec.RegistrationRef.Generation = 2 + if err := cl.Update(ctx, a); err != nil { + t.Fatalf("registration generation cannot advance: %v", err) + } a.Spec.RegistrationRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 690a4495..18e6441c 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -317,8 +317,9 @@ spec: rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0 - - message: contribution registration reference is immutable - rule: self.registrationRef == oldSelf.registrationRef + - message: contribution registration identity is immutable + rule: self.registrationRef.name == oldSelf.registrationRef.name && self.registrationRef.uid + == oldSelf.registrationRef.uid status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 2cfebb72..f7ae257d 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -164,7 +164,7 @@ spec: - type type: object type: array - observedSequence: + observedGeneration: format: int64 type: integer type: object diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index e07fa5a6..6611beba 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -50,6 +50,15 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm default: return kubeadmission.Allowed("API server authorization and schema validation apply") } + if req.Operation == admissionv1.Update { + var object metav1.PartialObjectMetadata + if err := json.Unmarshal(req.Object.Raw, &object); err != nil { + return kubeadmission.Denied("invalid resource metadata") + } + if !object.DeletionTimestamp.IsZero() { + return kubeadmission.Allowed("resource finalization does not require live references") + } + } cl := h.Client if h.ClientForRequest != nil { var err error diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 927fb676..e0151027 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -176,3 +176,13 @@ func TestContributionValidatesReferencesForRBACAuthorizedCallers(t *testing.T) { t.Fatal("contribution accepted a mismatched registration lifetime") } } + +func TestDeletingResourceCanRemoveFinalizersWithoutReferences(t *testing.T) { + h := Handler{} + for _, resource := range []string{"dnsregistrations", "dnsrecordcontributions", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings"} { + object := metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &metav1.Time{Time: time.Now()}}} + if res := h.Handle(context.Background(), request(resource, "publisher", object, object)); !res.Allowed { + t.Fatalf("%s finalization required live references: %v", resource, res.Result) + } + } +} From 29eea5fc554a9cd96f295bc445d13d39eaeeef12 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 21 Oct 2026 20:05:13 -0500 Subject: [PATCH 24/28] fix: retain DNS events across wire upgrades --- internal/internaldns/model/model.go | 18 ++--- internal/internaldns/transport/jetstream.go | 6 +- .../internaldns/transport/jetstream_test.go | 65 ++++++++++++++++++- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index ad551c24..5aae41d5 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 ( @@ -53,19 +55,19 @@ func NewEnvelope(kind Kind, eventID, region, shard, uid string, epoch, revision } 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 } diff --git a/internal/internaldns/transport/jetstream.go b/internal/internaldns/transport/jetstream.go index 8f300b8a..b7fdf591 100644 --- a/internal/internaldns/transport/jetstream.go +++ b/internal/internaldns/transport/jetstream.go @@ -295,7 +295,11 @@ func (c *Consumer) Next(ctx context.Context) (Message, error) { return nil, fmt.Errorf("decode envelope: %w", err) } if err := env.Validate(); err != nil { - _ = m.TermWithReason("invalid DNS envelope") + if errors.Is(err, model.ErrUnsupportedEnvelope) { + _ = m.NakWithDelay(time.Minute) + } else { + _ = m.TermWithReason("invalid DNS envelope") + } return nil, err } meta, err := m.Metadata() diff --git a/internal/internaldns/transport/jetstream_test.go b/internal/internaldns/transport/jetstream_test.go index 8a9e1456..2c9007ef 100644 --- a/internal/internaldns/transport/jetstream_test.go +++ b/internal/internaldns/transport/jetstream_test.go @@ -1,6 +1,15 @@ package transport -import "testing" +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() @@ -53,3 +62,57 @@ func TestBrokerMessageIDIsNamespacedBySubject(t *testing.T) { 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) + } + } +} From 679ca6a90628acbc68320651097d87ecc0649ba2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 09:29:27 -0500 Subject: [PATCH 25/28] test: Give envtest a minute to stop in the replicator tests The leader election tests failed on CI with "timeout waiting for process kube-apiserver to stop" after every assertion had passed: envtest gives the API server 20 seconds to exit, and a loaded runner can take longer while the managers' watch connections drain. Allow a minute so the cleanup does not fail a passing test. Co-Authored-By: Claude Opus 5.5 --- cmd/nonleader_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/nonleader_test.go b/cmd/nonleader_test.go index f50c3532..86af6d53 100644 --- a/cmd/nonleader_test.go +++ b/cmd/nonleader_test.go @@ -285,7 +285,9 @@ func annotatedWithin(ctx context.Context, t *testing.T, m *dnswebhook.DNSRecordS // the test ends. func startEnvtest(t *testing.T, dirs ...string) *rest.Config { t.Helper() - env := &envtest.Environment{CRDDirectoryPaths: dirs, ErrorIfCRDPathMissing: true} + // A loaded CI runner can take longer than envtest's 20 second default to + // stop an API server that still holds the managers' watch connections. + env := &envtest.Environment{CRDDirectoryPaths: dirs, ErrorIfCRDPathMissing: true, ControlPlaneStopTimeout: time.Minute} if dir := firstEnvTestBinaryDir(t); dir != "" { env.BinaryAssetsDirectory = dir } From 12d54afe05f1bbf9702711ec429938b784051055 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 10:17:43 -0500 Subject: [PATCH 26/28] test: Stop the replicator test managers before their API server The leader election tests still failed on CI with "timeout waiting for process kube-apiserver to stop" after a minute's stop timeout, so the API server was not slow but held open: the managers were started in goroutines nobody waited for, and they were still watching and releasing their leases when envtest stopped the API server. Each test now waits for its managers to return before envtest stops, and the stop timeout returns to envtest's default. Co-Authored-By: Claude Opus 5.5 --- cmd/leader_election_test.go | 2 +- cmd/nonleader_test.go | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) 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 86af6d53..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,13 +281,33 @@ 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 { t.Helper() - // A loaded CI runner can take longer than envtest's 20 second default to - // stop an API server that still holds the managers' watch connections. - env := &envtest.Environment{CRDDirectoryPaths: dirs, ErrorIfCRDPathMissing: true, ControlPlaneStopTimeout: time.Minute} + env := &envtest.Environment{CRDDirectoryPaths: dirs, ErrorIfCRDPathMissing: true} if dir := firstEnvTestBinaryDir(t); dir != "" { env.BinaryAssetsDirectory = dir } From b036a59da0bd9a767eb4cb3a4da324b1e96de9f3 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 12:19:41 -0500 Subject: [PATCH 27/28] fix: register IAM permissions for private DNS delivery resources DNSResolverBinding, DNSPublicationManifest, DNSPublicationChunk, DNSTransportOutbox and DNSPublicationOwnership had no ProtectedResource, so listing one in a project failed as "permission not registered" whatever the caller's role, and cleanup jobs that list every listable kind paged on-call. Register them like the other private DNS resources, with no role and Project as the parent, and a status subresource on the three kinds whose CRDs serve one. The internal-dns control plane keeps its access through its Kubernetes RBAC (config/internal-dns/rbac.yaml, added later in the stack). Co-Authored-By: Claude Opus 5.5 --- .../iam/protected-resources/internal-dns.yaml | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) 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 From 1cdece4ed34afd91826b1661245b2d0c8049b4c7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 14:39:21 -0500 Subject: [PATCH 28/28] fix: list an empty snapshot bucket and document the frozen envelope header `kv.Keys` returns `jetstream.ErrNoKeysFound` when a bucket has no keys, so `SnapshotStore.List` failed on the first bootstrap in a new region. An empty bucket now lists no snapshots. The envelope header rules run before the version gate on purpose. The header is frozen across wire versions, so a header that breaks them is malformed in every version and is terminated, while a well-formed header with an unknown apiVersion or kind is parked. The Envelope and Validate comments now say so, and a test pins both cases. Co-Authored-By: Claude Opus 5.5 --- internal/internaldns/model/model.go | 10 ++++++++++ internal/internaldns/model/model_test.go | 16 ++++++++++++++++ internal/internaldns/transport/jetstream.go | 4 ++++ internal/internaldns/transport/jetstream_test.go | 13 +++++++++++++ 4 files changed, 43 insertions(+) diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index 5aae41d5..cc29882b 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -32,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"` @@ -54,6 +59,11 @@ 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") 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 index b7fdf591..e62d0f9d 100644 --- a/internal/internaldns/transport/jetstream.go +++ b/internal/internaldns/transport/jetstream.go @@ -400,6 +400,10 @@ func (s *SnapshotStore) Get(ctx context.Context, key string) (model.Envelope, er 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 } diff --git a/internal/internaldns/transport/jetstream_test.go b/internal/internaldns/transport/jetstream_test.go index 2c9007ef..aa2e9e42 100644 --- a/internal/internaldns/transport/jetstream_test.go +++ b/internal/internaldns/transport/jetstream_test.go @@ -116,3 +116,16 @@ func TestMalformedEnvelopesAreTerminated(t *testing.T) { } } } + +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) + } +}