From 4c81fecc5aa21d8b454949e8c88dbf2151c2e22c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 19:01:47 -0500 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 69a8440981d13acd4adf5f85382de5c9b33c576c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 17:14:32 -0500 Subject: [PATCH 09/18] 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 695a45d15b9cc479025387cc67aa007d8de7af4f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 09:49:30 -0500 Subject: [PATCH 10/18] 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 11/18] 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 d4beb2fea949e942627394ae0371e6ec6824afc2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:29:26 -0500 Subject: [PATCH 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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) } }