From abf6995a3a25986c972329a260cf1acd2b9285e1 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 19:01:47 -0500 Subject: [PATCH 01/24] 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 1c14a23e5311018dea0eeb9b81b1c8a52e150541 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 19:11:00 -0500 Subject: [PATCH 02/24] 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 ad550b6925266991699a2044308b58f6d1d243bf Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 8 Oct 2026 20:11:18 -0500 Subject: [PATCH 03/24] 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 e29c489f9eaf7c000b4b9f14ed73b2e040c556b4 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 12:00:34 -0500 Subject: [PATCH 04/24] 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 fe465390..2c0141e0 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 d6c23b132fca9db345f151bc141db8f52c36281c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:05:37 -0500 Subject: [PATCH 05/24] 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 76db28d4af6c461a24a80f5bfd24edc1f47603de Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:34:43 -0500 Subject: [PATCH 06/24] 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 3bc1124c..e7a62fe8 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -26,3 +26,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 dda160bcb16f60cd1dc1810fb973d15be17447d7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 16:38:30 -0500 Subject: [PATCH 07/24] 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 e7a62fe8..f94e9c82 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -24,8 +24,6 @@ spec: - dns.networking.miloapis.com/dnszonediscoveries.get - dns.networking.miloapis.com/dnszonediscoveries/status.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 7c1951ee33c866461e038b12a6a49d97ba0565bc Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 17:07:23 -0500 Subject: [PATCH 08/24] 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 3c0a26126f61fed3f6be961caf0b56dc314bfab8 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 12 Oct 2026 17:14:32 -0500 Subject: [PATCH 09/24] 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 a94ef2acf42af813ca24a715d63272050d97e0b3 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 09:49:30 -0500 Subject: [PATCH 10/24] 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 ecf33ffd4fe6366378a42b774ce4dcd6188c3174 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 11:07:16 -0500 Subject: [PATCH 11/24] 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 9badbcae810fd7e1d6e638e907c0f7d4d4ad1c93 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:29:26 -0500 Subject: [PATCH 12/24] 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 b5cff18c02d6ebaa4a002ee4d69d2454453a3d8b Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:35:52 -0500 Subject: [PATCH 13/24] 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 f94e9c82..84c6a486 100644 --- a/config/iam/roles/dns-viewer.yaml +++ b/config/iam/roles/dns-viewer.yaml @@ -33,9 +33,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 e26515de962af5d81b59df65c7739a111a405dd7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:43:19 -0500 Subject: [PATCH 14/24] 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 7badaf40a207472b9ac4b9f229b7ed99853e058b Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:53:49 -0500 Subject: [PATCH 15/24] 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 c2ff425978aeddd772f316d5c1d3a8d38237126b Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 12:57:59 -0500 Subject: [PATCH 16/24] 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 63ae179adf4919855d4ca695e6873f370bba212f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 13 Oct 2026 13:44:21 -0500 Subject: [PATCH 17/24] feat: Simplify private DNS publication and access --- api/v1alpha1/internaldns_project_types.go | 101 ++------- .../internaldns_validation_envtest_test.go | 63 +++--- api/v1alpha1/zz_generated.deepcopy.go | 123 +---------- ...ing.miloapis.com_dnsmanagednamespaces.yaml | 191 ------------------ ...g.miloapis.com_dnsrecordcontributions.yaml | 22 +- ...working.miloapis.com_dnsregistrations.yaml | 6 - ...iloapis.com_dnsresolveraccessbindings.yaml | 64 +----- ...king.miloapis.com_dnsresolvercontexts.yaml | 3 - config/crd/kustomization.yaml | 3 +- internal/internaldns/admission/handler.go | 3 - .../internaldns/admission/handler_test.go | 5 +- internal/internaldns/model/model.go | 6 +- 12 files changed, 71 insertions(+), 519 deletions(-) delete mode 100644 config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 6cb1cbd0..bd49de5b 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -56,7 +56,6 @@ type DNSResolverContextManagedNamespaceStatus struct { type DNSResolverContextStatus struct { ManagedNamespace DNSResolverContextManagedNamespaceStatus `json:"managedNamespace,omitempty"` - AccessEpoch int64 `json:"accessEpoch,omitempty"` ServingTargets []DNSResolverContextServingTarget `json:"servingTargets,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -83,18 +82,6 @@ type DNSResolverContextList struct { Items []DNSResolverContext `json:"items"` } -// +kubebuilder:validation:Enum=DestinationAddress -type DNSResolverQueryIdentityType string - -const DNSResolverQueryIdentityDestinationAddress DNSResolverQueryIdentityType = "DestinationAddress" - -type DNSResolverQueryIdentity struct { - Type DNSResolverQueryIdentityType `json:"type"` - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - Value string `json:"value"` -} - type DNSResolverAccessVersion struct { // +kubebuilder:validation:Minimum=1 Epoch int64 `json:"epoch"` @@ -104,29 +91,22 @@ type DNSResolverAccessVersion struct { // +kubebuilder:validation:XValidation:rule="self.contextRef.name.size() > 0 && has(self.contextRef.uid) && self.contextRef.uid.size() > 0",message="contextRef requires name and UID" // +kubebuilder:validation:XValidation:rule="self.contextRef == oldSelf.contextRef",message="contextRef is immutable" -// +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity && self.port == oldSelf.port && self.transports == oldSelf.transports",message="access identity and transport are immutable" -// +kubebuilder:validation:XValidation:rule="self.version.epoch == oldSelf.version.epoch",message="epoch is immutable for an access binding" -// +kubebuilder:validation:XValidation:rule="self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence",message="version changes require an increasing sequence" +// +kubebuilder:validation:XValidation:rule="self.region == oldSelf.region && self.destinationAddress == oldSelf.destinationAddress",message="access destination is immutable" type DNSResolverAccessBindingSpec struct { ContextRef DNSObjectReference `json:"contextRef"` // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 - Region string `json:"region"` - QueryIdentity DNSResolverQueryIdentity `json:"queryIdentity"` - // +kubebuilder:validation:Enum=53 - Port int32 `json:"port"` - // +kubebuilder:validation:MinItems=2 - // +kubebuilder:validation:MaxItems=2 - // +kubebuilder:validation:items:Enum=UDP;TCP - // +listType=set - Transports []string `json:"transports"` - Version DNSResolverAccessVersion `json:"version"` + Region string `json:"region"` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="destinationAddress must be an IP address" + DestinationAddress string `json:"destinationAddress"` } type DNSResolverAccessBindingStatus struct { - ObservedSequence int64 `json:"observedSequence,omitempty"` - BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedSequence,omitempty"` + BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -163,22 +143,13 @@ type DNSZoneAssociationList struct { Items []DNSZoneAssociation `json:"items"` } -// +kubebuilder:validation:Enum=EligibleContributions;Persistent -type DNSPublicationPolicy string - -const ( - DNSPublicationPolicyEligibleContributions DNSPublicationPolicy = "EligibleContributions" - DNSPublicationPolicyPersistent DNSPublicationPolicy = "Persistent" -) - const DNSRegistrationMaxTTLSeconds int32 = 30 // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" type DNSRegistrationSpec struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - Name string `json:"name"` - RecordTypes []RRType `json:"recordTypes"` - PublicationPolicy DNSPublicationPolicy `json:"publicationPolicy"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + Name string `json:"name"` + RecordTypes []RRType `json:"recordTypes"` // TTLSeconds bounds cached dynamic answers to at most 30 seconds after withdrawal. // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=30 @@ -221,19 +192,14 @@ type DNSContributionRecordSet struct { type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` RecordSets []DNSContributionRecordSet `json:"recordSets"` + Eligible bool `json:"eligible"` + Reason string `json:"reason,omitempty"` } -// +kubebuilder:validation:XValidation:rule="!has(oldSelf.sequence) || (has(self.sequence) && self.sequence >= oldSelf.sequence)",message="observation sequence must not decrease" -// +kubebuilder:validation:XValidation:rule="(has(self.eligible) && self.eligible) == (has(oldSelf.eligible) && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) || self.sequence > oldSelf.sequence))",message="eligibility changes require a newer observation sequence" type DNSRecordContributionStatus struct { - // +kubebuilder:validation:Minimum=1 - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // +kubebuilder:validation:Minimum=1 - Sequence int64 `json:"sequence,omitempty"` - Eligible bool `json:"eligible,omitempty"` - Reason string `json:"reason,omitempty"` - PublishedRevision int64 `json:"publishedRevision,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + PublishedRevision int64 `json:"publishedRevision,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -335,37 +301,6 @@ type DNSNamingPolicyList struct { Items []DNSNamingPolicy `json:"items"` } -// DNSManagedNamespace tracks a resolver context's canonical private namespace. -type DNSManagedNamespaceSpec struct { - ProjectUID types.UID `json:"projectUID"` - ResolverContextRef DNSObjectReference `json:"resolverContextRef"` - DomainSuffix string `json:"domainSuffix"` - DNSZoneClassName string `json:"dnsZoneClassName"` -} - -type DNSManagedNamespaceStatus struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` - AssociationRef DNSObjectReference `json:"associationRef,omitempty"` - CanonicalSuffix string `json:"canonicalSuffix,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -type DNSManagedNamespace struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DNSManagedNamespaceSpec `json:"spec"` - Status DNSManagedNamespaceStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true -type DNSManagedNamespaceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []DNSManagedNamespace `json:"items"` -} - func init() { - SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}, &DNSManagedNamespace{}, &DNSManagedNamespaceList{}) + SchemeBuilder.Register(&DNSZoneAssociation{}, &DNSZoneAssociationList{}, &DNSResolverContext{}, &DNSResolverContextList{}, &DNSResolverAccessBinding{}, &DNSResolverAccessBindingList{}, &DNSRegistration{}, &DNSRegistrationList{}, &DNSRecordContribution{}, &DNSRecordContributionList{}, &DNSNamingPolicy{}, &DNSNamingPolicyList{}) } diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index fdcc64d0..99512988 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -55,9 +55,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { } } newAccess := func(name string) *DNSResolverAccessBinding { - return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ - ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", QueryIdentity: DNSResolverQueryIdentity{Type: DNSResolverQueryIdentityDestinationAddress, Value: "fd53::1"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: DNSResolverAccessVersion{Epoch: 1, Sequence: 1}, - }} + return &DNSResolverAccessBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "validation"}, Spec: DNSResolverAccessBindingSpec{ContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, Region: "central", DestinationAddress: "fd53::1"}} } for _, tc := range []struct { name string @@ -66,14 +64,8 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { {"empty-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "" }}, {"missing-context-uid", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.UID = "" }}, {"empty-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "" }}, - {"empty-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "" }}, - {"unsupported-identity", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Type = "SourceAddress" }}, - {"wrong-port", func(a *DNSResolverAccessBinding) { a.Spec.Port = 5353 }}, - {"missing-tcp", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP"} }}, - {"duplicate-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "UDP"} }}, - {"unsupported-transport", func(a *DNSResolverAccessBinding) { a.Spec.Transports = []string{"UDP", "TLS"} }}, - {"zero-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch = 0 }}, - {"zero-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 0 }}, + {"empty-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "" }}, + {"invalid-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "resolver.internal" }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name); tc.mutate(a); requireInvalid(t, cl.Create(ctx, a)) }) } @@ -83,13 +75,10 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { }{ {"change-context", func(a *DNSResolverAccessBinding) { a.Spec.ContextRef.Name = "other" }}, {"change-region", func(a *DNSResolverAccessBinding) { a.Spec.Region = "east" }}, - {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.QueryIdentity.Value = "fd53::2" }}, - {"change-epoch", func(a *DNSResolverAccessBinding) { a.Spec.Version.Epoch++ }}, - {"decrease-sequence", func(a *DNSResolverAccessBinding) { a.Spec.Version.Sequence = 1 }}, + {"change-address", func(a *DNSResolverAccessBinding) { a.Spec.DestinationAddress = "fd53::2" }}, } { t.Run(tc.name, func(t *testing.T) { a := newAccess(tc.name) - a.Spec.Version.Sequence = 2 if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } @@ -97,18 +86,17 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { requireInvalid(t, cl.Update(ctx, a)) }) } - t.Run("sequence-and-status", func(t *testing.T) { + t.Run("metadata-and-status", func(t *testing.T) { a := newAccess("renewal") if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Spec.Version.Sequence++ if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } - a.Status.ObservedSequence = a.Spec.Version.Sequence + a.Status.ObservedGeneration = a.Generation if err := cl.Status().Update(ctx, a); err != nil { - t.Fatalf("unchanged access version blocked status update: %v", err) + t.Fatalf("access status update failed: %v", err) } a.Labels = map[string]string{"test": "metadata"} if err := cl.Update(ctx, a); err != nil { @@ -127,7 +115,7 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { }) for _, ttl := range []int32{0, 31, 2147483647} { t.Run("ttl", func(t *testing.T) { - a := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, PublicationPolicy: DNSPublicationPolicyEligibleContributions, TTLSeconds: ttl}} + a := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, TTLSeconds: ttl}} requireInvalid(t, cl.Create(ctx, a)) }) } @@ -139,30 +127,37 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { a.Spec.RegistrationRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) - t.Run("contribution-observation-order", func(t *testing.T) { - a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "observations", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}}} + t.Run("atomic-contribution-and-stale-update", func(t *testing.T) { + a := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{Name: "observations", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{}, Eligible: true}} if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } - a.Status = DNSRecordContributionStatus{ObservedGeneration: a.Generation, Sequence: 2, Eligible: true} - if err := cl.Status().Update(ctx, a); err != nil { + stale := a.DeepCopy() + generation := a.Generation + a.Spec.Eligible = false + a.Spec.Reason = "EndpointUnhealthy" + a.Spec.RecordSets = []DNSContributionRecordSet{{RecordType: RRTypeA, Records: []RecordEntry{{Name: "web", A: &ARecordSpec{Content: "10.0.0.9"}}}}} + if err := cl.Update(ctx, a); err != nil { t.Fatal(err) } - decreasing := a.DeepCopy() - decreasing.Status.Sequence = 1 - requireInvalid(t, cl.Status().Update(ctx, decreasing)) - unchanged := a.DeepCopy() - unchanged.Status.Eligible = false - requireInvalid(t, cl.Status().Update(ctx, unchanged)) - a.Status.Sequence++ - a.Status.Eligible = false - if err := cl.Status().Update(ctx, a); err != nil { - t.Fatal(err) + if a.Generation != generation+1 { + t.Fatalf("atomic desired state did not advance generation: %d", a.Generation) } + if err := cl.Update(ctx, stale); !apierrors.IsConflict(err) { + t.Fatalf("stale writer was not rejected: %v", err) + } + a.Status.ObservedGeneration = a.Generation a.Status.Conditions = []metav1.Condition{{Type: "Published", Status: metav1.ConditionTrue, Reason: "Applied", LastTransitionTime: metav1.Now()}} if err := cl.Status().Update(ctx, a); err != nil { t.Fatal(err) } + var observed DNSRecordContribution + if err := cl.Get(ctx, client.ObjectKeyFromObject(a), &observed); err != nil { + t.Fatal(err) + } + if observed.Spec.Eligible || observed.Spec.RecordSets[0].Records[0].A.Content != "10.0.0.9" { + t.Fatal("status write changed producer desired state") + } }) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 066a3987..df717379 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -130,105 +130,6 @@ func (in *DNSContributionRecordSet) DeepCopy() *DNSContributionRecordSet { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespace) DeepCopyInto(out *DNSManagedNamespace) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespace. -func (in *DNSManagedNamespace) DeepCopy() *DNSManagedNamespace { - if in == nil { - return nil - } - out := new(DNSManagedNamespace) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSManagedNamespace) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespaceList) DeepCopyInto(out *DNSManagedNamespaceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DNSManagedNamespace, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceList. -func (in *DNSManagedNamespaceList) DeepCopy() *DNSManagedNamespaceList { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DNSManagedNamespaceList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespaceSpec) DeepCopyInto(out *DNSManagedNamespaceSpec) { - *out = *in - out.ResolverContextRef = in.ResolverContextRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceSpec. -func (in *DNSManagedNamespaceSpec) DeepCopy() *DNSManagedNamespaceSpec { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSManagedNamespaceStatus) DeepCopyInto(out *DNSManagedNamespaceStatus) { - *out = *in - out.DNSZoneRef = in.DNSZoneRef - out.AssociationRef = in.AssociationRef - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSManagedNamespaceStatus. -func (in *DNSManagedNamespaceStatus) DeepCopy() *DNSManagedNamespaceStatus { - if in == nil { - return nil - } - out := new(DNSManagedNamespaceStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSNamingPolicy) DeepCopyInto(out *DNSNamingPolicy) { *out = *in @@ -755,7 +656,7 @@ func (in *DNSResolverAccessBinding) DeepCopyInto(out *DNSResolverAccessBinding) *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) + out.Spec = in.Spec in.Status.DeepCopyInto(&out.Status) } @@ -813,13 +714,6 @@ func (in *DNSResolverAccessBindingList) DeepCopyObject() runtime.Object { func (in *DNSResolverAccessBindingSpec) DeepCopyInto(out *DNSResolverAccessBindingSpec) { *out = *in out.ContextRef = in.ContextRef - out.QueryIdentity = in.QueryIdentity - if in.Transports != nil { - in, out := &in.Transports, &out.Transports - *out = make([]string, len(*in)) - copy(*out, *in) - } - out.Version = in.Version } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverAccessBindingSpec. @@ -1023,21 +917,6 @@ func (in *DNSResolverContextStatus) DeepCopy() *DNSResolverContextStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DNSResolverQueryIdentity) DeepCopyInto(out *DNSResolverQueryIdentity) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSResolverQueryIdentity. -func (in *DNSResolverQueryIdentity) DeepCopy() *DNSResolverQueryIdentity { - if in == nil { - return nil - } - out := new(DNSResolverQueryIdentity) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSZone) DeepCopyInto(out *DNSZone) { *out = *in diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml deleted file mode 100644 index c57ef52a..00000000 --- a/config/crd/bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml +++ /dev/null @@ -1,191 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: dnsmanagednamespaces.dns.networking.miloapis.com -spec: - group: dns.networking.miloapis.com - names: - kind: DNSManagedNamespace - listKind: DNSManagedNamespaceList - plural: dnsmanagednamespaces - singular: dnsmanagednamespace - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: DNSManagedNamespace tracks a resolver context's canonical - private namespace. - properties: - dnsZoneClassName: - type: string - domainSuffix: - type: string - projectUID: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - type: string - resolverContextRef: - description: |- - DNSObjectReference pins a namespaced object by API-assigned UID. Generation - is used where a writer must also be fenced against policy changes. - properties: - generation: - format: int64 - minimum: 0 - type: integer - name: - maxLength: 253 - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - maxLength: 128 - type: string - required: - - name - type: object - required: - - dnsZoneClassName - - domainSuffix - - projectUID - - resolverContextRef - type: object - status: - properties: - associationRef: - description: |- - DNSObjectReference pins a namespaced object by API-assigned UID. Generation - is used where a writer must also be fenced against policy changes. - properties: - generation: - format: int64 - minimum: 0 - type: integer - name: - maxLength: 253 - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - maxLength: 128 - type: string - required: - - name - type: object - canonicalSuffix: - type: string - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - dnsZoneRef: - description: |- - DNSObjectReference pins a namespaced object by API-assigned UID. Generation - is used where a writer must also be fenced against policy changes. - properties: - generation: - format: int64 - minimum: 0 - type: integer - name: - maxLength: 253 - type: string - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - maxLength: 128 - type: string - required: - - name - type: object - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 2ef3f510..690a4495 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -38,6 +38,10 @@ spec: type: object spec: properties: + eligible: + type: boolean + reason: + type: string recordSets: items: properties: @@ -304,6 +308,7 @@ spec: - name type: object required: + - eligible - recordSets - registrationRef type: object @@ -372,30 +377,13 @@ spec: - type type: object type: array - eligible: - type: boolean observedGeneration: format: int64 - minimum: 1 type: integer publishedRevision: format: int64 type: integer - reason: - type: string - sequence: - format: int64 - minimum: 1 - type: integer type: object - x-kubernetes-validations: - - message: observation sequence must not decrease - rule: '!has(oldSelf.sequence) || (has(self.sequence) && self.sequence - >= oldSelf.sequence)' - - message: eligibility changes require a newer observation sequence - rule: (has(self.eligible) && self.eligible) == (has(oldSelf.eligible) - && oldSelf.eligible) || (has(self.sequence) && (!has(oldSelf.sequence) - || self.sequence > oldSelf.sequence)) required: - spec type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml index 701b0e8a..2eca271a 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -62,11 +62,6 @@ spec: type: object name: type: string - publicationPolicy: - enum: - - EligibleContributions - - Persistent - type: string recordTypes: items: enum: @@ -100,7 +95,6 @@ spec: required: - dnsZoneRef - name - - publicationPolicy - recordTypes - ttlSeconds type: object diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 45c904b9..2cfebb72 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -60,60 +60,21 @@ spec: required: - name type: object - port: - enum: - - 53 - format: int32 - type: integer - queryIdentity: - properties: - type: - enum: - - DestinationAddress - type: string - value: - maxLength: 253 - minLength: 1 - type: string - required: - - type - - value - type: object + destinationAddress: + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: destinationAddress must be an IP address + rule: isIP(self) region: maxLength: 253 minLength: 1 type: string - transports: - items: - enum: - - UDP - - TCP - type: string - maxItems: 2 - minItems: 2 - type: array - x-kubernetes-list-type: set - version: - properties: - epoch: - format: int64 - minimum: 1 - type: integer - sequence: - format: int64 - minimum: 1 - type: integer - required: - - epoch - - sequence - type: object required: - contextRef - - port - - queryIdentity + - destinationAddress - region - - transports - - version type: object x-kubernetes-validations: - message: contextRef requires name and UID @@ -121,13 +82,8 @@ spec: self.contextRef.uid.size() > 0 - message: contextRef is immutable rule: self.contextRef == oldSelf.contextRef - - message: access identity and transport are immutable - rule: self.region == oldSelf.region && self.queryIdentity == oldSelf.queryIdentity - && self.port == oldSelf.port && self.transports == oldSelf.transports - - message: epoch is immutable for an access binding - rule: self.version.epoch == oldSelf.version.epoch - - message: version changes require an increasing sequence - rule: self.version == oldSelf.version || self.version.sequence > oldSelf.version.sequence + - message: access destination is immutable + rule: self.region == oldSelf.region && self.destinationAddress == oldSelf.destinationAddress status: properties: bindingRef: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml index 2bdf3da0..64cc3207 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolvercontexts.yaml @@ -58,9 +58,6 @@ spec: rule: oldSelf.consumerID == self.consumerID status: properties: - accessEpoch: - format: int64 - type: integer conditions: items: description: Condition contains details for one aspect of the current diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 8d7e5599..81e6435a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,7 +6,8 @@ resources: - bases/dns.networking.miloapis.com_dnszones.yaml - bases/dns.networking.miloapis.com_dnsrecordsets.yaml - bases/dns.networking.miloapis.com_dnszonediscoveries.yaml -- bases/dns.networking.miloapis.com_dnsmanagednamespaces.yaml + + - bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml - bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml - bases/dns.networking.miloapis.com_dnsregistrations.yaml diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index bc191f32..e07fa5a6 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -94,9 +94,6 @@ func (h *Handler) resolverAccess(ctx context.Context, cl client.Client, req kube if resolverContext.UID != current.Spec.ContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() { return fmt.Errorf("resolver context lifetime is not current") } - if resolverContext.Status.AccessEpoch < 1 || current.Spec.Version.Epoch != resolverContext.Status.AccessEpoch { - return fmt.Errorf("access version does not match the current context epoch") - } ready := apimeta.FindStatusCondition(resolverContext.Status.Conditions, "Ready") if ready == nil || ready.Status != metav1.ConditionTrue || ready.ObservedGeneration != resolverContext.Generation { return fmt.Errorf("resolver context is not ready for its current generation") diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 416bf6ed..927fb676 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -88,12 +88,11 @@ func TestResolverAccessRequiresCurrentContext(t *testing.T) { if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } - resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{AccessEpoch: 3}} + resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}, Status: dnsv1alpha1.DNSResolverContextStatus{}} setAdmissionCondition(&resolverContext.Status.Conditions, "Ready", metav1.ConditionTrue, 1, now) h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(resolverContext).Build()} - old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", QueryIdentity: dnsv1alpha1.DNSResolverQueryIdentity{Type: dnsv1alpha1.DNSResolverQueryIdentityDestinationAddress, Value: "fd70:100::10"}, Port: 53, Transports: []string{"UDP", "TCP"}, Version: dnsv1alpha1.DNSResolverAccessVersion{Epoch: 3, Sequence: 9}}} + old := &dnsv1alpha1.DNSResolverAccessBinding{Spec: dnsv1alpha1.DNSResolverAccessBindingSpec{ContextRef: dnsv1alpha1.DNSObjectReference{Name: "ctx", UID: "ctx-uid"}, Region: "central", DestinationAddress: "fd70:100::10"}} current := old.DeepCopy() - current.Spec.Version.Sequence = 10 req := request("dnsresolveraccessbindings", "system:serviceaccount:network-system:integration", current, old) if res := h.Handle(context.Background(), req); !res.Allowed { t.Fatalf("current integration update denied: %v", res.Result) diff --git a/internal/internaldns/model/model.go b/internal/internaldns/model/model.go index a7257475..1f613b98 100644 --- a/internal/internaldns/model/model.go +++ b/internal/internaldns/model/model.go @@ -17,7 +17,7 @@ import ( "github.com/miekg/dns" ) -const APIVersion = "internal.dns.networking.miloapis.com/v1alpha5" +const APIVersion = "internal.dns.networking.miloapis.com/v1alpha6" type Kind string @@ -349,6 +349,8 @@ type ObservationFence struct { ContributionUID string `json:"contributionUID"` Generation uint64 `json:"generation"` Sequence uint64 `json:"sequence"` + Retired bool `json:"retired,omitempty"` + SourceName string `json:"sourceName,omitempty"` } func (p PublicationPlan) Validate() error { @@ -415,7 +417,7 @@ func (p PublicationPlan) Validate() error { } } else { observation, ok := observationByUID[rec.ContributionUID] - if !ok || rec.Generation != observation.Generation || rec.Sequence != observation.Sequence { + if !ok || observation.Retired || rec.Generation != observation.Generation || rec.Sequence != observation.Sequence { return fmt.Errorf("RRset %q record fence does not match contribution observation %q", key, rec.ContributionUID) } } From 9b8b32b60f6cf0a7ddc022967fdab72ad4b39a62 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 21 Oct 2026 19:55:49 -0500 Subject: [PATCH 18/24] fix: allow DNS contribution refresh and cleanup --- api/v1alpha1/internaldns_project_types.go | 4 ++-- api/v1alpha1/internaldns_validation_envtest_test.go | 4 ++++ ...networking.miloapis.com_dnsrecordcontributions.yaml | 5 +++-- ...working.miloapis.com_dnsresolveraccessbindings.yaml | 2 +- internal/internaldns/admission/handler.go | 9 +++++++++ internal/internaldns/admission/handler_test.go | 10 ++++++++++ 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index bd49de5b..3792af66 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -104,7 +104,7 @@ type DNSResolverAccessBindingSpec struct { } type DNSResolverAccessBindingStatus struct { - ObservedGeneration int64 `json:"observedSequence,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` BindingRef *DNSObjectReference `json:"bindingRef,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -188,7 +188,7 @@ type DNSContributionRecordSet struct { } // +kubebuilder:validation:XValidation:rule="self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0",message="registrationRef requires name, UID, and generation" -// +kubebuilder:validation:XValidation:rule="self.registrationRef == oldSelf.registrationRef",message="contribution registration reference is immutable" +// +kubebuilder:validation:XValidation:rule="self.registrationRef.name == oldSelf.registrationRef.name && self.registrationRef.uid == oldSelf.registrationRef.uid",message="contribution registration identity is immutable" type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` RecordSets []DNSContributionRecordSet `json:"recordSets"` diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index 99512988..6fc45c37 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -124,6 +124,10 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { if err := cl.Create(ctx, a); err != nil { t.Fatal(err) } + a.Spec.RegistrationRef.Generation = 2 + if err := cl.Update(ctx, a); err != nil { + t.Fatalf("registration generation cannot advance: %v", err) + } a.Spec.RegistrationRef.Name = "other" requireInvalid(t, cl.Update(ctx, a)) }) diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 690a4495..18e6441c 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -317,8 +317,9 @@ spec: rule: self.registrationRef.name.size() > 0 && has(self.registrationRef.uid) && self.registrationRef.uid.size() > 0 && has(self.registrationRef.generation) && self.registrationRef.generation > 0 - - message: contribution registration reference is immutable - rule: self.registrationRef == oldSelf.registrationRef + - message: contribution registration identity is immutable + rule: self.registrationRef.name == oldSelf.registrationRef.name && self.registrationRef.uid + == oldSelf.registrationRef.uid status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml index 2cfebb72..f7ae257d 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsresolveraccessbindings.yaml @@ -164,7 +164,7 @@ spec: - type type: object type: array - observedSequence: + observedGeneration: format: int64 type: integer type: object diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index e07fa5a6..6611beba 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -50,6 +50,15 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm default: return kubeadmission.Allowed("API server authorization and schema validation apply") } + if req.Operation == admissionv1.Update { + var object metav1.PartialObjectMetadata + if err := json.Unmarshal(req.Object.Raw, &object); err != nil { + return kubeadmission.Denied("invalid resource metadata") + } + if !object.DeletionTimestamp.IsZero() { + return kubeadmission.Allowed("resource finalization does not require live references") + } + } cl := h.Client if h.ClientForRequest != nil { var err error diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index 927fb676..e0151027 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -176,3 +176,13 @@ func TestContributionValidatesReferencesForRBACAuthorizedCallers(t *testing.T) { t.Fatal("contribution accepted a mismatched registration lifetime") } } + +func TestDeletingResourceCanRemoveFinalizersWithoutReferences(t *testing.T) { + h := Handler{} + for _, resource := range []string{"dnsregistrations", "dnsrecordcontributions", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings"} { + object := metav1.PartialObjectMetadata{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &metav1.Time{Time: time.Now()}}} + if res := h.Handle(context.Background(), request(resource, "publisher", object, object)); !res.Allowed { + t.Fatalf("%s finalization required live references: %v", resource, res.Result) + } + } +} From 469e97df5eeb5082a1fc1452d1c5adf1da1fa83b Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 09:25:55 -0500 Subject: [PATCH 19/24] chore: Remove committed internal DNS kind output The `.internal-dns-e2e/` directory is local output from the internal DNS kind harness, including kubeconfigs with client keys, and nothing in the repository reads it. Remove it and ignore the directory so it cannot be committed again. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + .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 - 13 files changed, 3 insertions(+), 8828 deletions(-) delete mode 100644 .internal-dns-e2e/kubernetes/crds.yaml delete mode 100644 .internal-dns-e2e/kubernetes/image.id delete mode 100644 .internal-dns-e2e/kubernetes/kind-config.yaml delete mode 100644 .internal-dns-e2e/kubernetes/platform-bootstrap.json delete mode 100644 .internal-dns-e2e/kubernetes/platform-internal.kubeconfig delete mode 100644 .internal-dns-e2e/kubernetes/platform.kubeconfig delete mode 100644 .internal-dns-e2e/kubernetes/project-a-bootstrap.json delete mode 100644 .internal-dns-e2e/kubernetes/project-a-internal.kubeconfig delete mode 100644 .internal-dns-e2e/kubernetes/project-a.kubeconfig delete mode 100644 .internal-dns-e2e/kubernetes/project-b-bootstrap.json delete mode 100644 .internal-dns-e2e/kubernetes/project-b-internal.kubeconfig delete mode 100644 .internal-dns-e2e/kubernetes/project-b.kubeconfig diff --git a/.gitignore b/.gitignore index b48f9e6e..50697f11 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ go.work.sum .claude/settings.local.json .claude/worktrees/ .mcp.json + +# Local kind output from the internal DNS end-to-end harness (includes kubeconfigs) +.internal-dns-e2e/ diff --git a/.internal-dns-e2e/kubernetes/crds.yaml b/.internal-dns-e2e/kubernetes/crds.yaml deleted file mode 100644 index 00cbe9d7..00000000 --- a/.internal-dns-e2e/kubernetes/crds.yaml +++ /dev/null @@ -1,4219 +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: {} ---- -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 deleted file mode 100644 index a3d10dcc..00000000 --- a/.internal-dns-e2e/kubernetes/image.id +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 7ce1ca91..00000000 --- a/.internal-dns-e2e/kubernetes/kind-config.yaml +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 6b185976..00000000 --- a/.internal-dns-e2e/kubernetes/platform-bootstrap.json +++ /dev/null @@ -1,2566 +0,0 @@ -{ - "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 deleted file mode 100644 index 33b19f20..00000000 --- a/.internal-dns-e2e/kubernetes/platform-internal.kubeconfig +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 2889e57a..00000000 --- a/.internal-dns-e2e/kubernetes/platform.kubeconfig +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 228cb7ea..00000000 --- a/.internal-dns-e2e/kubernetes/project-a-bootstrap.json +++ /dev/null @@ -1,946 +0,0 @@ -{ - "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 deleted file mode 100644 index 07ced9a1..00000000 --- a/.internal-dns-e2e/kubernetes/project-a-internal.kubeconfig +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 6a1bd2c6..00000000 --- a/.internal-dns-e2e/kubernetes/project-a.kubeconfig +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index f3cd22b3..00000000 --- a/.internal-dns-e2e/kubernetes/project-b-bootstrap.json +++ /dev/null @@ -1,946 +0,0 @@ -{ - "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 deleted file mode 100644 index 11f2efe0..00000000 --- a/.internal-dns-e2e/kubernetes/project-b-internal.kubeconfig +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 2ea48d3e..00000000 --- a/.internal-dns-e2e/kubernetes/project-b.kubeconfig +++ /dev/null @@ -1,18 +0,0 @@ -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== From 3df4347c15d895538468fdb549255d1285bd1974 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 09:25:55 -0500 Subject: [PATCH 20/24] feat: Serve the private DNS reference checks from the dns-operator webhook The `/validate-internal-dns` handler was never registered, so its same-namespace, UID, private-zone and record-owner checks ran in no shipped configuration. The replicator's webhook server now serves it beside the DNSRecordSet webhooks, and both the same-cluster webhook manifests and the control-plane `admission-webhooks` component register it for DNSZoneAssociation, DNSRegistration, DNSRecordContribution, DNSNamingPolicy and DNSResolverAccessBinding writes with `failurePolicy: Fail`. Unlike the DNSRecordSet owner name guard, nothing downstream re-checks these tenant and lifetime boundaries, so a write must not be admitted when the webhook is unreachable. The handler reads the project control plane named by the request's Milo parent context through its API reader rather than the informer cache, since it compares UIDs and generations a cache can serve stale. A request without project context reads the local cluster, and a named project the replicator cannot find fails the request instead of being checked against another cluster. The manager role gains `get` on registrations and resolver contexts for those reads. The handler's resource list is now exported so a test can hold both webhook registrations to exactly the resources the handler checks, and the served-webhook test posts to the path through the same setup the replicator runs. Co-Authored-By: Claude Opus 5.5 --- cmd/main.go | 10 +-- .../components/admission-webhooks/README.md | 4 +- .../validating-webhook-configuration.yaml | 31 ++++++++ config/rbac/role.yaml | 7 ++ config/webhook/manifests.yaml | 24 ++++++ internal/internaldns/admission/handler.go | 11 ++- internal/webhook/internaldns.go | 76 ++++++++++++++++++ internal/webhook/internaldns_manifest_test.go | 74 ++++++++++++++++++ internal/webhook/server_test.go | 78 ++++++++++++++++++- 9 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 internal/webhook/internaldns.go create mode 100644 internal/webhook/internaldns_manifest_test.go diff --git a/cmd/main.go b/cmd/main.go index fbe725df..cac16db1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -366,7 +366,7 @@ func main() { } if err := setupReplicatorWebhook(mcmgr); err != nil { - setupLog.Error(err, "unable to set up the DNSRecordSet webhook and its checks") + setupLog.Error(err, "unable to set up the admission webhooks and their checks") os.Exit(1) } @@ -405,11 +405,11 @@ func newReplicatorManager( return runnables, mcmgr, nil } -// setupReplicatorWebhook registers the DNSRecordSet webhooks, the -// replicator's health check, and a readiness check on its webhook server. +// setupReplicatorWebhook registers the DNSRecordSet and private DNS webhooks, +// the replicator's health check, and a readiness check on its webhook server. func setupReplicatorWebhook(mcmgr mcmanager.Manager) error { - if err := dnswebhook.SetupDNSRecordSetWebhook(mcmgr); err != nil { - return fmt.Errorf("unable to create the DNSRecordSet webhook: %w", err) + if err := dnswebhook.SetupWebhooks(mcmgr); err != nil { + return fmt.Errorf("unable to create the admission webhooks: %w", err) } if err := mcmgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { return fmt.Errorf("unable to set up health check: %w", err) diff --git a/config/components/admission-webhooks/README.md b/config/components/admission-webhooks/README.md index 1af771fc..96390bdd 100644 --- a/config/components/admission-webhooks/README.md +++ b/config/components/admission-webhooks/README.md @@ -31,6 +31,8 @@ again. A guard that blocked every DNS write while the webhook was unreachable would trade a late condition for the outage in #69, and the per-record `NotOwner` condition still reports anything that slips through. +The same `ValidatingWebhookConfiguration` registers `/validate-internal-dns` for the five private DNS resources that reference other objects: `DNSZoneAssociation`, `DNSRegistration`, `DNSRecordContribution`, `DNSNamingPolicy` and `DNSResolverAccessBinding`. It checks that each reference names a live object in the same project namespace with the pinned UID, that zones are private, and that record owners and types fit the registration. Its `failurePolicy` is `Fail`: nothing downstream re-checks these tenant and lifetime boundaries, so a write must not be admitted without them. + This directory is **not** included in `config/default` or the replicator overlay. Same-cluster packaging for kind/e2e stays under `config/webhook/`. @@ -39,7 +41,7 @@ overlay. Same-cluster packaging for kind/e2e stays under `config/webhook/`. | Kind | Name | Notes | | ---- | ---- | ----- | | `MutatingWebhookConfiguration` | `dns-operator-mutating-webhook-configuration` | `failurePolicy: Fail`; Service ref is `dns-operator-webhook-service` / `datum-dns-system` | -| `ValidatingWebhookConfiguration` | `dns-operator-validating-webhook-configuration` | `failurePolicy: Ignore`; same Service ref | +| `ValidatingWebhookConfiguration` | `dns-operator-validating-webhook-configuration` | `vdnsrecordset.kb.io` is `failurePolicy: Ignore`; `vinternaldns.kb.io` is `failurePolicy: Fail`; same Service ref | No Service and no kustomize `namespace` transformer: Flux `targetNamespace` must not rewrite `clientConfig.service.namespace`. diff --git a/config/components/admission-webhooks/validating-webhook-configuration.yaml b/config/components/admission-webhooks/validating-webhook-configuration.yaml index d126deb2..32e55ce6 100644 --- a/config/components/admission-webhooks/validating-webhook-configuration.yaml +++ b/config/components/admission-webhooks/validating-webhook-configuration.yaml @@ -29,3 +29,34 @@ webhooks: resources: - dnsrecordsets scope: "*" + # Private DNS reference checks. Unlike the owner name guard, these decide + # tenant and lifetime boundaries nothing downstream re-checks, so a write + # must not be admitted without them. + - name: vinternaldns.kb.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: dns-operator-webhook-service + namespace: datum-dns-system + path: /validate-internal-dns + port: 443 + failurePolicy: Fail + matchPolicy: Equivalent + sideEffects: None + timeoutSeconds: 10 + rules: + - apiGroups: + - dns.networking.miloapis.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - dnszoneassociations + - dnsregistrations + - dnsrecordcontributions + - dnsnamingpolicies + - dnsresolveraccessbindings + scope: Namespaced diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d8c1ac83..f378240c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -56,6 +56,13 @@ rules: - get - patch - update +- apiGroups: + - dns.networking.miloapis.com + resources: + - dnsregistrations + - dnsresolvercontexts + verbs: + - get - apiGroups: - dns.networking.miloapis.com resources: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index de68d030..ec7e19b0 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -50,3 +50,27 @@ webhooks: resources: - dnsrecordsets sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-internal-dns + failurePolicy: Fail + name: vinternaldns.kb.io + rules: + - apiGroups: + - dns.networking.miloapis.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - dnszoneassociations + - dnsregistrations + - dnsrecordcontributions + - dnsnamingpolicies + - dnsresolveraccessbindings + sideEffects: None diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 6611beba..5a1d5faf 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" "go.miloapis.com/dns-operator/internal/internaldns/model" @@ -22,8 +23,13 @@ const subresourceStatus = "status" const contributionResource = "dnsrecordcontributions" +// Path is where the dns-operator webhook server serves this handler. const Path = "/validate-internal-dns" +// Resources lists the resources whose writes require live reference checks. +// The ValidatingWebhookConfiguration must match exactly these resources. +var Resources = []string{"dnszoneassociations", "dnsregistrations", contributionResource, "dnsnamingpolicies", "dnsresolveraccessbindings"} + // Handler uses an uncached client selected by authenticated project context. type Handler struct { Client client.Client @@ -44,10 +50,7 @@ func (h *Handler) Handle(ctx context.Context, req kubeadmission.Request) kubeadm if req.SubResource == subresourceStatus { return kubeadmission.Allowed("API server status authorization applies") } - switch req.Resource.Resource { - case "dnsregistrations", contributionResource, "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings": - // These resources require live references. - default: + if !slices.Contains(Resources, req.Resource.Resource) { return kubeadmission.Allowed("API server authorization and schema validation apply") } if req.Operation == admissionv1.Update { diff --git a/internal/webhook/internaldns.go b/internal/webhook/internaldns.go new file mode 100644 index 00000000..2cf36385 --- /dev/null +++ b/internal/webhook/internaldns.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package webhook + +import ( + "context" + "fmt" + "strings" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + internaldnsadmission "go.miloapis.com/dns-operator/internal/internaldns/admission" +) + +// The private DNS reference checks fail closed: a write that skipped them +// could bind a registration, contribution, association, naming policy, or +// access binding to another tenant's or a replaced object. +// +kubebuilder:webhook:path=/validate-internal-dns,mutating=false,failurePolicy=fail,sideEffects=None,groups=dns.networking.miloapis.com,resources=dnszoneassociations;dnsregistrations;dnsrecordcontributions;dnsnamingpolicies;dnsresolveraccessbindings,verbs=create;update,versions=v1alpha1,name=vinternaldns.kb.io,admissionReviewVersions=v1 +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones;dnsregistrations;dnsresolvercontexts,verbs=get + +// SetupWebhooks registers every admission handler the dns-operator serves. +func SetupWebhooks(mgr mcmanager.Manager) error { + if err := SetupDNSRecordSetWebhook(mgr); err != nil { + return err + } + SetupInternalDNSWebhook(mgr) + return nil +} + +// SetupInternalDNSWebhook serves the private DNS reference checks at +// internaldnsadmission.Path. +func SetupInternalDNSWebhook(mgr mcmanager.Manager) { + mgr.GetWebhookServer().Register(internaldnsadmission.Path, &admission.Webhook{ + Handler: &internaldnsadmission.Handler{ClientForRequest: liveProjectClient(mgr)}, + }) +} + +// liveProjectClient selects the project control plane named by the request's +// cluster context and reads it without a cache, since the checks compare UIDs +// and generations a cache could serve stale. A request without project context +// reads the local cluster. A named project that cannot be found fails the +// request instead of falling back to another cluster. +func liveProjectClient(mgr mcmanager.Manager) func(context.Context, admission.Request) (client.Client, error) { + return func(ctx context.Context, _ admission.Request) (client.Client, error) { + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok || clusterName == "" { + local := mgr.GetLocalManager() + return liveReads{Client: local.GetClient(), reader: local.GetAPIReader()}, nil + } + cl, err := mgr.GetCluster(ctx, clusterName) + if err != nil && !strings.HasPrefix(clusterName.String(), "/") { + cl, err = mgr.GetCluster(ctx, "/"+clusterName) + } + if err != nil { + return nil, fmt.Errorf("project control plane %q is unavailable: %w", clusterName, err) + } + return liveReads{Client: cl.GetClient(), reader: cl.GetAPIReader()}, nil + } +} + +// liveReads sends reads to the API server instead of the informer cache. +type liveReads struct { + client.Client + reader client.Reader +} + +func (l liveReads) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + return l.reader.Get(ctx, key, obj, opts...) +} + +func (l liveReads) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return l.reader.List(ctx, list, opts...) +} diff --git a/internal/webhook/internaldns_manifest_test.go b/internal/webhook/internaldns_manifest_test.go new file mode 100644 index 00000000..8433df94 --- /dev/null +++ b/internal/webhook/internaldns_manifest_test.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package webhook + +import ( + "bytes" + "os" + "path/filepath" + "slices" + "testing" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + internaldnsadmission "go.miloapis.com/dns-operator/internal/internaldns/admission" +) + +// Both the same-cluster and the control-plane registrations must send every +// private DNS reference write to the handler and fail closed. +func TestInternalDNSWebhookIsRegisteredFailClosed(t *testing.T) { + for _, path := range []string{ + filepath.Join("..", "..", "config", "webhook", "manifests.yaml"), + filepath.Join("..", "..", "config", "components", "admission-webhooks", "validating-webhook-configuration.yaml"), + } { + t.Run(path, func(t *testing.T) { + hook := findValidatingWebhook(t, path, internaldnsadmission.Path) + if hook.FailurePolicy == nil || *hook.FailurePolicy != admissionregistrationv1.Fail { + t.Errorf("failurePolicy = %v, want Fail", hook.FailurePolicy) + } + if len(hook.Rules) != 1 { + t.Fatalf("rules = %d, want 1", len(hook.Rules)) + } + rule := hook.Rules[0] + if !slices.Equal(rule.APIGroups, []string{dnsv1alpha1.GroupVersion.Group}) { + t.Errorf("apiGroups = %v", rule.APIGroups) + } + ops := []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update} + if !slices.Equal(rule.Operations, ops) { + t.Errorf("operations = %v, want %v", rule.Operations, ops) + } + got := slices.Sorted(slices.Values(rule.Resources)) + want := slices.Sorted(slices.Values(internaldnsadmission.Resources)) + if !slices.Equal(got, want) { + t.Errorf("resources = %v, want the handled resources %v", got, want) + } + }) + } +} + +func findValidatingWebhook(t *testing.T, path, servicePath string) admissionregistrationv1.ValidatingWebhook { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + decoder := utilyaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 4096) + for { + var config admissionregistrationv1.ValidatingWebhookConfiguration + if err := decoder.Decode(&config); err != nil { + break + } + if config.Kind != "ValidatingWebhookConfiguration" { + continue + } + for _, hook := range config.Webhooks { + if hook.ClientConfig.Service != nil && hook.ClientConfig.Service.Path != nil && *hook.ClientConfig.Service.Path == servicePath { + return hook + } + } + } + t.Fatalf("%s registers no validating webhook for %s", path, servicePath) + return admissionregistrationv1.ValidatingWebhook{} +} diff --git a/internal/webhook/server_test.go b/internal/webhook/server_test.go index 92917d0d..d6e60005 100644 --- a/internal/webhook/server_test.go +++ b/internal/webhook/server_test.go @@ -16,6 +16,7 @@ import ( "time" admissionv1 "k8s.io/api/admission/v1" + authv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -33,9 +34,11 @@ import ( const ( mutatePath = "/mutate-dns-networking-miloapis-com-v1alpha1-dnsrecordset" validatePath = "/validate-dns-networking-miloapis-com-v1alpha1-dnsrecordset" + // Kept literal so the test fails if the served path drifts from the manifests. + internalDNSPath = "/validate-internal-dns" ) -func TestSetupDNSRecordSetWebhook_IsServed(t *testing.T) { +func TestSetupWebhooks_AreServed(t *testing.T) { scheme := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(scheme); err != nil { t.Fatalf("AddToScheme: %v", err) @@ -75,8 +78,8 @@ func TestSetupDNSRecordSetWebhook_IsServed(t *testing.T) { if err != nil { t.Fatalf("creating multicluster manager: %v", err) } - if err := SetupDNSRecordSetWebhook(mgr); err != nil { - t.Fatalf("SetupDNSRecordSetWebhook: %v", err) + if err := SetupWebhooks(mgr); err != nil { + t.Fatalf("SetupWebhooks: %v", err) } ctx, cancel := context.WithCancel(context.Background()) @@ -167,6 +170,64 @@ func TestSetupDNSRecordSetWebhook_IsServed(t *testing.T) { } }) + privateZone := &dnsv1alpha1.DNSZone{ + ObjectMeta: metav1.ObjectMeta{Name: "private-zone", Namespace: "default"}, + Spec: dnsv1alpha1.DNSZoneSpec{DomainName: "internal.example", DNSZoneClassName: "private", Visibility: dnsv1alpha1.DNSZoneVisibilityPrivate}, + } + if err := k8sClient.Create(ctx, privateZone); err != nil { + t.Fatalf("creating private DNSZone: %v", err) + } + registrationReview := func(zoneUID types.UID, extra map[string]authv1.ExtraValue) admissionv1.AdmissionReview { + registration := &dnsv1alpha1.DNSRegistration{ + ObjectMeta: metav1.ObjectMeta{Name: "web", Namespace: "default"}, + Spec: dnsv1alpha1.DNSRegistrationSpec{ + DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: privateZone.Name, UID: zoneUID}, + Name: "web", + RecordTypes: []dnsv1alpha1.RRType{dnsv1alpha1.RRTypeA}, + TTLSeconds: 30, + }, + } + raw, err := json.Marshal(registration) + if err != nil { + t.Fatalf("marshalling DNSRegistration: %v", err) + } + return admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, + Request: &admissionv1.AdmissionRequest{ + UID: "internal-dns-uid", + Operation: admissionv1.Create, + Namespace: registration.Namespace, + Name: registration.Name, + Kind: metav1.GroupVersionKind{Group: dnsv1alpha1.GroupVersion.Group, Version: dnsv1alpha1.GroupVersion.Version, Kind: "DNSRegistration"}, + Resource: metav1.GroupVersionResource{Group: dnsv1alpha1.GroupVersion.Group, Version: dnsv1alpha1.GroupVersion.Version, Resource: "dnsregistrations"}, + UserInfo: authv1.UserInfo{Username: "project-admin", Extra: extra}, + Object: runtime.RawExtension{Raw: raw}, + }, + } + } + + t.Run("internal DNS path admits a live private zone reference", func(t *testing.T) { + resp := sendAdmissionReview(ctx, t, httpClient, endpoint(internalDNSPath), registrationReview(privateZone.UID, nil)) + if !resp.Allowed { + t.Fatalf("internal DNS webhook refused a live private zone reference: %v", resp.Result) + } + }) + + t.Run("internal DNS path refuses a replaced zone", func(t *testing.T) { + resp := sendAdmissionReview(ctx, t, httpClient, endpoint(internalDNSPath), registrationReview("replaced-zone-uid", nil)) + if resp.Allowed { + t.Fatal("internal DNS webhook admitted a reference to a replaced zone") + } + }) + + t.Run("internal DNS path fails closed for an unknown project", func(t *testing.T) { + extra := map[string]authv1.ExtraValue{ParentTypeExtraKey: {"Project"}, ParentNameExtraKey: {"missing-project"}} + resp := sendAdmissionReview(ctx, t, httpClient, endpoint(internalDNSPath), registrationReview(privateZone.UID, extra)) + if resp.Allowed { + t.Fatal("internal DNS webhook checked an unknown project's write against the local cluster") + } + }) + cancel() select { case err := <-runErr: @@ -237,6 +298,17 @@ func postAdmissionReview( } review.Request.OldObject = runtime.RawExtension{Raw: oldRaw} } + return sendAdmissionReview(ctx, t, httpClient, url, review) +} + +func sendAdmissionReview( + ctx context.Context, + t *testing.T, + httpClient *http.Client, + url string, + review admissionv1.AdmissionReview, +) *admissionv1.AdmissionResponse { + t.Helper() body, err := json.Marshal(review) if err != nil { From ef371468ceeea2fac9fee5f88b462b98df958c11 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 09:25:55 -0500 Subject: [PATCH 21/24] fix: Pin private DNS reference identity and bound registration lists DNSZoneAssociation and DNSNamingPolicy references to their zone and resolver context, and a DNSRegistration's zone reference and owner name, could be edited after creation, unlike the other private DNS types. Their name and UID are now immutable while a pinned generation may still advance, so a zone edit does not lock the objects that reference it. DNSRegistration's owner name now has the DNSRecordSet owner name pattern and a 253 character limit, its record types are a nonempty set of at most 14, and its reserved descendants a set of at most 64 names. A contribution holds at most 14 record sets of at most 256 records, and a naming policy at most 256 additional name rules. Tightening these after v1alpha1 ships would be a breaking change. DNSZoneAssociationSpec.Managed is removed. Nothing read it, and as a spec field the dns-admin role could set it although DNS owns managed state; managed associations are already identified by their controller owner reference and the managed label. Co-Authored-By: Claude Opus 5.5 --- api/v1alpha1/internaldns_project_types.go | 51 ++++++--- .../internaldns_validation_envtest_test.go | 106 ++++++++++++++++++ ...orking.miloapis.com_dnsnamingpolicies.yaml | 9 ++ ...g.miloapis.com_dnsrecordcontributions.yaml | 2 + ...working.miloapis.com_dnsregistrations.yaml | 17 +++ ...king.miloapis.com_dnszoneassociations.yaml | 8 +- 6 files changed, 175 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 3792af66..6a30378a 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -22,10 +22,11 @@ type DNSObjectReference struct { // +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" +// +kubebuilder:validation:XValidation:rule="self.dnsZoneRef.name == oldSelf.dnsZoneRef.name && self.dnsZoneRef.uid == oldSelf.dnsZoneRef.uid",message="dnsZoneRef identity is immutable" +// +kubebuilder:validation:XValidation:rule="self.resolverContextRef.name == oldSelf.resolverContextRef.name && self.resolverContextRef.uid == oldSelf.resolverContextRef.uid",message="resolverContextRef identity is immutable" type DNSZoneAssociationSpec struct { DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` ResolverContextRef DNSObjectReference `json:"resolverContextRef"` - Managed bool `json:"managed,omitempty"` } type DNSZoneAssociationStatus struct { @@ -146,14 +147,27 @@ type DNSZoneAssociationList struct { const DNSRegistrationMaxTTLSeconds int32 = 30 // +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.dnsZoneRef.name == oldSelf.dnsZoneRef.name && self.dnsZoneRef.uid == oldSelf.dnsZoneRef.uid",message="dnsZoneRef identity is immutable" +// +kubebuilder:validation:XValidation:rule="self.name == oldSelf.name",message="name is immutable" type DNSRegistrationSpec struct { - DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` - Name string `json:"name"` - RecordTypes []RRType `json:"recordTypes"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + // Name is the owner name, relative to the zone or fully qualified. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^(@|[A-Za-z0-9*._-]+)$` + Name string `json:"name"` + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=14 + // +listType=set + RecordTypes []RRType `json:"recordTypes"` // TTLSeconds bounds cached dynamic answers to at most 30 seconds after withdrawal. // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=30 - TTLSeconds int32 `json:"ttlSeconds"` + TTLSeconds int32 `json:"ttlSeconds"` + // +kubebuilder:validation:MaxItems=64 + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=253 + // +listType=set ReservedDescendants []string `json:"reservedDescendants,omitempty"` } @@ -183,17 +197,19 @@ type DNSRegistrationList struct { } type DNSContributionRecordSet struct { - RecordType RRType `json:"recordType"` - Records []RecordEntry `json:"records"` + RecordType RRType `json:"recordType"` + // +kubebuilder:validation:MaxItems=256 + 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.registrationRef.name == oldSelf.registrationRef.name && self.registrationRef.uid == oldSelf.registrationRef.uid",message="contribution registration identity is immutable" type DNSRecordContributionSpec struct { - RegistrationRef DNSObjectReference `json:"registrationRef"` - RecordSets []DNSContributionRecordSet `json:"recordSets"` - Eligible bool `json:"eligible"` - Reason string `json:"reason,omitempty"` + RegistrationRef DNSObjectReference `json:"registrationRef"` + // +kubebuilder:validation:MaxItems=14 + RecordSets []DNSContributionRecordSet `json:"recordSets"` + Eligible bool `json:"eligible"` + Reason string `json:"reason,omitempty"` } type DNSRecordContributionStatus struct { @@ -222,12 +238,15 @@ type DNSRecordContributionList struct { // +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" +// +kubebuilder:validation:XValidation:rule="self.resolverContextRef.name == oldSelf.resolverContextRef.name && self.resolverContextRef.uid == oldSelf.resolverContextRef.uid",message="resolverContextRef identity is immutable" +// +kubebuilder:validation:XValidation:rule="(has(self.dnsZoneRef) ? self.dnsZoneRef.name : \"\") == (has(oldSelf.dnsZoneRef) ? oldSelf.dnsZoneRef.name : \"\") && ((has(self.dnsZoneRef) && has(self.dnsZoneRef.uid)) ? self.dnsZoneRef.uid : \"\") == ((has(oldSelf.dnsZoneRef) && has(oldSelf.dnsZoneRef.uid)) ? oldSelf.dnsZoneRef.uid : \"\")",message="dnsZoneRef identity is immutable" type DNSNamingPolicySpec struct { - ResolverContextRef DNSObjectReference `json:"resolverContextRef"` - AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` - DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` - ScopePrefix string `json:"scopePrefix,omitempty"` - Priority int32 `json:"priority,omitempty"` + ResolverContextRef DNSObjectReference `json:"resolverContextRef"` + // +kubebuilder:validation:MaxItems=256 + AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` + DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + ScopePrefix string `json:"scopePrefix,omitempty"` + Priority int32 `json:"priority,omitempty"` } // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" diff --git a/api/v1alpha1/internaldns_validation_envtest_test.go b/api/v1alpha1/internaldns_validation_envtest_test.go index 6fc45c37..106a41f3 100644 --- a/api/v1alpha1/internaldns_validation_envtest_test.go +++ b/api/v1alpha1/internaldns_validation_envtest_test.go @@ -6,6 +6,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" corev1 "k8s.io/api/core/v1" @@ -163,5 +164,110 @@ func TestPrivateDNSValidationAtAPIServer(t *testing.T) { t.Fatal("status write changed producer desired state") } }) + validateReferenceIdentity(t, cl, requireInvalid) + validateRegistrationBounds(t, cl, requireInvalid) +} + +// References may advance a pinned generation, but never retarget another object. +func validateReferenceIdentity(t *testing.T, cl client.Client, requireInvalid func(*testing.T, error)) { + ctx := context.Background() + zone := DNSObjectReference{Name: "zone", UID: "zone-uid", Generation: 1} + resolverContext := DNSObjectReference{Name: "context", UID: "context-uid"} + t.Run("association-reference-identity", func(t *testing.T) { + for name, mutate := range map[string]func(*DNSZoneAssociation){ + "zone-name": func(a *DNSZoneAssociation) { a.Spec.DNSZoneRef.Name = "other" }, + "zone-uid": func(a *DNSZoneAssociation) { a.Spec.DNSZoneRef.UID = "other-uid" }, + "context-name": func(a *DNSZoneAssociation) { a.Spec.ResolverContextRef.Name = "other" }, + "context-uid": func(a *DNSZoneAssociation) { a.Spec.ResolverContextRef.UID = "other-uid" }, + } { + a := &DNSZoneAssociation{ObjectMeta: metav1.ObjectMeta{GenerateName: "association-", Namespace: "validation"}, Spec: DNSZoneAssociationSpec{DNSZoneRef: zone, ResolverContextRef: resolverContext}} + if err := cl.Create(ctx, a); err != nil { + t.Fatal(err) + } + a.Spec.DNSZoneRef.Generation = 2 + if err := cl.Update(ctx, a); err != nil { + t.Fatalf("zone generation cannot advance: %v", err) + } + mutate(a) + t.Run(name, func(t *testing.T) { requireInvalid(t, cl.Update(ctx, a)) }) + } + }) + t.Run("naming-policy-reference-identity", func(t *testing.T) { + for name, mutate := range map[string]func(*DNSNamingPolicy){ + "zone-name": func(p *DNSNamingPolicy) { p.Spec.DNSZoneRef.Name = "other" }, + "zone-uid": func(p *DNSNamingPolicy) { p.Spec.DNSZoneRef.UID = "other-uid" }, + "zone-cleared": func(p *DNSNamingPolicy) { p.Spec.DNSZoneRef = DNSObjectReference{} }, + "context-name": func(p *DNSNamingPolicy) { p.Spec.ResolverContextRef.Name = "other" }, + "context-uid": func(p *DNSNamingPolicy) { p.Spec.ResolverContextRef.UID = "other-uid" }, + } { + p := &DNSNamingPolicy{ObjectMeta: metav1.ObjectMeta{GenerateName: "naming-", Namespace: "validation"}, Spec: DNSNamingPolicySpec{ResolverContextRef: resolverContext, DNSZoneRef: zone, AdditionalNames: []DNSAdditionalNameRule{}}} + if err := cl.Create(ctx, p); err != nil { + t.Fatal(err) + } + p.Spec.Priority = 10 + if err := cl.Update(ctx, p); err != nil { + t.Fatalf("naming policy cannot change unpinned fields: %v", err) + } + mutate(p) + t.Run(name, func(t *testing.T) { requireInvalid(t, cl.Update(ctx, p)) }) + } + p := &DNSNamingPolicy{ObjectMeta: metav1.ObjectMeta{GenerateName: "naming-", Namespace: "validation"}, Spec: DNSNamingPolicySpec{ResolverContextRef: resolverContext, AdditionalNames: []DNSAdditionalNameRule{}}} + if err := cl.Create(ctx, p); err != nil { + t.Fatal(err) + } + p.Spec.DNSZoneRef = zone + t.Run("zone-selected-later", func(t *testing.T) { requireInvalid(t, cl.Update(ctx, p)) }) + }) + t.Run("registration-identity", func(t *testing.T) { + for name, mutate := range map[string]func(*DNSRegistration){ + "name": func(r *DNSRegistration) { r.Spec.Name = "api" }, + "zone-name": func(r *DNSRegistration) { r.Spec.DNSZoneRef.Name = "other" }, + "zone-uid": func(r *DNSRegistration) { r.Spec.DNSZoneRef.UID = "other-uid" }, + } { + r := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "registration-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: zone, Name: "web", RecordTypes: []RRType{RRTypeAAAA}, TTLSeconds: 30}} + if err := cl.Create(ctx, r); err != nil { + t.Fatal(err) + } + r.Spec.TTLSeconds = 10 + r.Spec.DNSZoneRef.Generation = 2 + if err := cl.Update(ctx, r); err != nil { + t.Fatalf("registration cannot change TTL or zone generation: %v", err) + } + mutate(r) + t.Run(name, func(t *testing.T) { requireInvalid(t, cl.Update(ctx, r)) }) + } + }) +} +func validateRegistrationBounds(t *testing.T, cl client.Client, requireInvalid func(*testing.T, error)) { + ctx := context.Background() + many := func(n int, prefix string) []string { + out := make([]string, n) + for i := range out { + out[i] = prefix + string(rune('a'+i%26)) + string(rune('a'+i/26)) + } + return out + } + for name, mutate := range map[string]func(*DNSRegistrationSpec){ + "empty-name": func(s *DNSRegistrationSpec) { s.Name = "" }, + "name-with-space": func(s *DNSRegistrationSpec) { s.Name = "web app" }, + "long-name": func(s *DNSRegistrationSpec) { s.Name = strings.Repeat("a", 254) }, + "no-record-types": func(s *DNSRegistrationSpec) { s.RecordTypes = nil }, + "duplicate-record-type": func(s *DNSRegistrationSpec) { s.RecordTypes = []RRType{RRTypeA, RRTypeA} }, + "many-reserved": func(s *DNSRegistrationSpec) { s.ReservedDescendants = many(65, "r") }, + } { + t.Run("registration-bounds-"+name, func(t *testing.T) { + r := &DNSRegistration{ObjectMeta: metav1.ObjectMeta{GenerateName: "bounded-", Namespace: "validation"}, Spec: DNSRegistrationSpec{DNSZoneRef: DNSObjectReference{Name: "zone", UID: "zone-uid"}, Name: "web", RecordTypes: []RRType{RRTypeA}, TTLSeconds: 30}} + mutate(&r.Spec) + requireInvalid(t, cl.Create(ctx, r)) + }) + } + t.Run("contribution-bounds", func(t *testing.T) { + records := make([]RecordEntry, 257) + for i := range records { + records[i] = RecordEntry{Name: "web", A: &ARecordSpec{Content: "10.0.0.1"}} + } + c := &DNSRecordContribution{ObjectMeta: metav1.ObjectMeta{GenerateName: "bounded-", Namespace: "validation"}, Spec: DNSRecordContributionSpec{RegistrationRef: DNSObjectReference{Name: "web", UID: "registration-uid", Generation: 1}, RecordSets: []DNSContributionRecordSet{{RecordType: RRTypeA, Records: records}}}} + requireInvalid(t, cl.Create(ctx, c)) + }) } diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml index e835d310..f25d5887 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -116,6 +116,7 @@ spec: - message: dnsZoneRef must pin the zone UID rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 + maxItems: 256 type: array dnsZoneRef: description: |- @@ -183,6 +184,14 @@ spec: - message: resolverContextRef is required rule: self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 + - message: resolverContextRef identity is immutable + rule: self.resolverContextRef.name == oldSelf.resolverContextRef.name + && self.resolverContextRef.uid == oldSelf.resolverContextRef.uid + - message: dnsZoneRef identity is immutable + rule: '(has(self.dnsZoneRef) ? self.dnsZoneRef.name : "") == (has(oldSelf.dnsZoneRef) + ? oldSelf.dnsZoneRef.name : "") && ((has(self.dnsZoneRef) && has(self.dnsZoneRef.uid)) + ? self.dnsZoneRef.uid : "") == ((has(oldSelf.dnsZoneRef) && has(oldSelf.dnsZoneRef.uid)) + ? oldSelf.dnsZoneRef.uid : "")' status: properties: conditions: diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 18e6441c..27571e98 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -279,11 +279,13 @@ spec: required: - name type: object + maxItems: 256 type: array required: - recordType - records type: object + maxItems: 14 type: array registrationRef: description: |- diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml index 2eca271a..b98c6c95 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsregistrations.yaml @@ -61,6 +61,11 @@ spec: - name type: object name: + description: Name is the owner name, relative to the zone or fully + qualified. + maxLength: 253 + minLength: 1 + pattern: ^(@|[A-Za-z0-9*._-]+)$ type: string recordTypes: items: @@ -80,11 +85,18 @@ spec: - HTTPS - SVCB type: string + maxItems: 14 + minItems: 1 type: array + x-kubernetes-list-type: set reservedDescendants: items: + maxLength: 253 + minLength: 1 type: string + maxItems: 64 type: array + x-kubernetes-list-type: set ttlSeconds: description: TTLSeconds bounds cached dynamic answers to at most 30 seconds after withdrawal. @@ -101,6 +113,11 @@ spec: x-kubernetes-validations: - message: dnsZoneRef must pin the zone UID rule: has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0 + - message: dnsZoneRef identity is immutable + rule: self.dnsZoneRef.name == oldSelf.dnsZoneRef.name && self.dnsZoneRef.uid + == oldSelf.dnsZoneRef.uid + - message: name is immutable + rule: self.name == oldSelf.name 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 36b832a2..14653ea2 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnszoneassociations.yaml @@ -60,8 +60,6 @@ spec: required: - name type: object - managed: - type: boolean resolverContextRef: description: |- DNSObjectReference pins a namespaced object by API-assigned UID. Generation @@ -100,6 +98,12 @@ spec: - message: resolverContextRef is required rule: self.resolverContextRef.name.size() > 0 && has(self.resolverContextRef.uid) && self.resolverContextRef.uid.size() > 0 + - message: dnsZoneRef identity is immutable + rule: self.dnsZoneRef.name == oldSelf.dnsZoneRef.name && self.dnsZoneRef.uid + == oldSelf.dnsZoneRef.uid + - message: resolverContextRef identity is immutable + rule: self.resolverContextRef.name == oldSelf.resolverContextRef.name + && self.resolverContextRef.uid == oldSelf.resolverContextRef.uid status: properties: bindingRef: From bcf021fbcb86efbd022cfd37584e8ea6af393b9b Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 11:33:29 -0500 Subject: [PATCH 22/24] fix: leave the deployed internal DNS webhook registration to the control plane In deployed environments the internal-dns control plane serves and registers /validate-internal-dns with failurePolicy Fail. Registering the same handler again from the dns-operator admission-webhooks component meant every private DNS write passed through two fail-closed webhooks, so an outage of either server blocked those writes. The component now leaves that registration out and says why. The replicator still serves the handler, and config/webhook still registers it for the kind and e2e overlays built from config/default. The manifest test now checks that the deployed component does not register the path and that the kind and e2e configuration does. Co-Authored-By: Claude Opus 5.5 --- .../components/admission-webhooks/README.md | 4 +- .../validating-webhook-configuration.yaml | 36 ++------ internal/webhook/internaldns.go | 4 +- internal/webhook/internaldns_manifest_test.go | 82 +++++++++++-------- 4 files changed, 59 insertions(+), 67 deletions(-) diff --git a/config/components/admission-webhooks/README.md b/config/components/admission-webhooks/README.md index 96390bdd..d989f38e 100644 --- a/config/components/admission-webhooks/README.md +++ b/config/components/admission-webhooks/README.md @@ -31,7 +31,7 @@ again. A guard that blocked every DNS write while the webhook was unreachable would trade a late condition for the outage in #69, and the per-record `NotOwner` condition still reports anything that slips through. -The same `ValidatingWebhookConfiguration` registers `/validate-internal-dns` for the five private DNS resources that reference other objects: `DNSZoneAssociation`, `DNSRegistration`, `DNSRecordContribution`, `DNSNamingPolicy` and `DNSResolverAccessBinding`. It checks that each reference names a live object in the same project namespace with the pinned UID, that zones are private, and that record owners and types fit the registration. Its `failurePolicy` is `Fail`: nothing downstream re-checks these tenant and lifetime boundaries, so a write must not be admitted without them. +This component does not register `/validate-internal-dns`, the private DNS reference checks for `DNSZoneAssociation`, `DNSRegistration`, `DNSRecordContribution`, `DNSNamingPolicy` and `DNSResolverAccessBinding`. In deployed environments the internal-dns control plane serves and registers that handler with `failurePolicy: Fail` (the `config/internal-dns` admission added in #238, and infra's `internal-dns-admission`). A second fail-closed registration here would block those writes whenever either server was unavailable. The dns-operator replicator still serves the handler, and `config/webhook` registers it for kind and e2e. This directory is **not** included in `config/default` or the replicator overlay. Same-cluster packaging for kind/e2e stays under `config/webhook/`. @@ -41,7 +41,7 @@ overlay. Same-cluster packaging for kind/e2e stays under `config/webhook/`. | Kind | Name | Notes | | ---- | ---- | ----- | | `MutatingWebhookConfiguration` | `dns-operator-mutating-webhook-configuration` | `failurePolicy: Fail`; Service ref is `dns-operator-webhook-service` / `datum-dns-system` | -| `ValidatingWebhookConfiguration` | `dns-operator-validating-webhook-configuration` | `vdnsrecordset.kb.io` is `failurePolicy: Ignore`; `vinternaldns.kb.io` is `failurePolicy: Fail`; same Service ref | +| `ValidatingWebhookConfiguration` | `dns-operator-validating-webhook-configuration` | `vdnsrecordset.kb.io` is `failurePolicy: Ignore`; same Service ref | No Service and no kustomize `namespace` transformer: Flux `targetNamespace` must not rewrite `clientConfig.service.namespace`. diff --git a/config/components/admission-webhooks/validating-webhook-configuration.yaml b/config/components/admission-webhooks/validating-webhook-configuration.yaml index 32e55ce6..0e8f9ffd 100644 --- a/config/components/admission-webhooks/validating-webhook-configuration.yaml +++ b/config/components/admission-webhooks/validating-webhook-configuration.yaml @@ -29,34 +29,8 @@ webhooks: resources: - dnsrecordsets scope: "*" - # Private DNS reference checks. Unlike the owner name guard, these decide - # tenant and lifetime boundaries nothing downstream re-checks, so a write - # must not be admitted without them. - - name: vinternaldns.kb.io - admissionReviewVersions: - - v1 - clientConfig: - service: - name: dns-operator-webhook-service - namespace: datum-dns-system - path: /validate-internal-dns - port: 443 - failurePolicy: Fail - matchPolicy: Equivalent - sideEffects: None - timeoutSeconds: 10 - rules: - - apiGroups: - - dns.networking.miloapis.com - apiVersions: - - v1alpha1 - operations: - - CREATE - - UPDATE - resources: - - dnszoneassociations - - dnsregistrations - - dnsrecordcontributions - - dnsnamingpolicies - - dnsresolveraccessbindings - scope: Namespaced + # No /validate-internal-dns entry: in deployed environments the internal-dns + # control plane serves and registers the private DNS reference checks + # (config/internal-dns admission from #238, infra's internal-dns-admission). A second + # fail-closed registration here would block those writes whenever either + # server is unavailable. config/webhook keeps it for kind and e2e. diff --git a/internal/webhook/internaldns.go b/internal/webhook/internaldns.go index 2cf36385..438b69cc 100644 --- a/internal/webhook/internaldns.go +++ b/internal/webhook/internaldns.go @@ -17,7 +17,9 @@ import ( // The private DNS reference checks fail closed: a write that skipped them // could bind a registration, contribution, association, naming policy, or -// access binding to another tenant's or a replaced object. +// access binding to another tenant's or a replaced object. This marker +// registers them for kind and e2e only; in deployed environments the +// internal-dns control plane registers the same path. // +kubebuilder:webhook:path=/validate-internal-dns,mutating=false,failurePolicy=fail,sideEffects=None,groups=dns.networking.miloapis.com,resources=dnszoneassociations;dnsregistrations;dnsrecordcontributions;dnsnamingpolicies;dnsresolveraccessbindings,verbs=create;update,versions=v1alpha1,name=vinternaldns.kb.io,admissionReviewVersions=v1 // +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones;dnsregistrations;dnsresolvercontexts,verbs=get diff --git a/internal/webhook/internaldns_manifest_test.go b/internal/webhook/internaldns_manifest_test.go index 8433df94..fc999a51 100644 --- a/internal/webhook/internaldns_manifest_test.go +++ b/internal/webhook/internaldns_manifest_test.go @@ -4,6 +4,8 @@ package webhook import ( "bytes" + "errors" + "io" "os" "path/filepath" "slices" @@ -16,59 +18,73 @@ import ( internaldnsadmission "go.miloapis.com/dns-operator/internal/internaldns/admission" ) -// Both the same-cluster and the control-plane registrations must send every -// private DNS reference write to the handler and fail closed. +// The kind and e2e registration must send every private DNS reference write to +// the handler and fail closed. func TestInternalDNSWebhookIsRegisteredFailClosed(t *testing.T) { - for _, path := range []string{ - filepath.Join("..", "..", "config", "webhook", "manifests.yaml"), - filepath.Join("..", "..", "config", "components", "admission-webhooks", "validating-webhook-configuration.yaml"), - } { - t.Run(path, func(t *testing.T) { - hook := findValidatingWebhook(t, path, internaldnsadmission.Path) - if hook.FailurePolicy == nil || *hook.FailurePolicy != admissionregistrationv1.Fail { - t.Errorf("failurePolicy = %v, want Fail", hook.FailurePolicy) - } - if len(hook.Rules) != 1 { - t.Fatalf("rules = %d, want 1", len(hook.Rules)) - } - rule := hook.Rules[0] - if !slices.Equal(rule.APIGroups, []string{dnsv1alpha1.GroupVersion.Group}) { - t.Errorf("apiGroups = %v", rule.APIGroups) - } - ops := []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update} - if !slices.Equal(rule.Operations, ops) { - t.Errorf("operations = %v, want %v", rule.Operations, ops) - } - got := slices.Sorted(slices.Values(rule.Resources)) - want := slices.Sorted(slices.Values(internaldnsadmission.Resources)) - if !slices.Equal(got, want) { - t.Errorf("resources = %v, want the handled resources %v", got, want) - } - }) + path := filepath.Join("..", "..", "config", "webhook", "manifests.yaml") + hook, ok := findValidatingWebhook(t, path, internaldnsadmission.Path) + if !ok { + t.Fatalf("%s registers no validating webhook for %s", path, internaldnsadmission.Path) + } + if hook.FailurePolicy == nil || *hook.FailurePolicy != admissionregistrationv1.Fail { + t.Errorf("failurePolicy = %v, want Fail", hook.FailurePolicy) + } + if len(hook.Rules) != 1 { + t.Fatalf("rules = %d, want 1", len(hook.Rules)) + } + rule := hook.Rules[0] + if !slices.Equal(rule.APIGroups, []string{dnsv1alpha1.GroupVersion.Group}) { + t.Errorf("apiGroups = %v", rule.APIGroups) + } + ops := []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update} + if !slices.Equal(rule.Operations, ops) { + t.Errorf("operations = %v, want %v", rule.Operations, ops) + } + got := slices.Sorted(slices.Values(rule.Resources)) + want := slices.Sorted(slices.Values(internaldnsadmission.Resources)) + if !slices.Equal(got, want) { + t.Errorf("resources = %v, want the handled resources %v", got, want) } } -func findValidatingWebhook(t *testing.T, path, servicePath string) admissionregistrationv1.ValidatingWebhook { +// In deployed environments the internal-dns control plane registers the +// private DNS reference checks. A second fail-closed registration from the +// control-plane component would block those writes whenever either server is +// unavailable. +func TestDeployedComponentLeavesInternalDNSWebhookToControlPlane(t *testing.T) { + path := filepath.Join("..", "..", "config", "components", "admission-webhooks", "validating-webhook-configuration.yaml") + if _, ok := findValidatingWebhook(t, path, internaldnsadmission.Path); ok { + t.Fatalf("%s registers %s; the internal-dns control plane owns that registration", path, internaldnsadmission.Path) + } +} + +func findValidatingWebhook(t *testing.T, path, servicePath string) (admissionregistrationv1.ValidatingWebhook, bool) { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } decoder := utilyaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 4096) + found := false for { var config admissionregistrationv1.ValidatingWebhookConfiguration - if err := decoder.Decode(&config); err != nil { + if err := decoder.Decode(&config); errors.Is(err, io.EOF) { break + } else if err != nil { + t.Fatalf("decoding %s: %v", path, err) } if config.Kind != "ValidatingWebhookConfiguration" { continue } + found = true for _, hook := range config.Webhooks { if hook.ClientConfig.Service != nil && hook.ClientConfig.Service.Path != nil && *hook.ClientConfig.Service.Path == servicePath { - return hook + return hook, true } } } - t.Fatalf("%s registers no validating webhook for %s", path, servicePath) - return admissionregistrationv1.ValidatingWebhook{} + if !found { + t.Fatalf("%s contains no ValidatingWebhookConfiguration", path) + } + return admissionregistrationv1.ValidatingWebhook{}, false } From 1ebeca98f8bd5ad8618ce94e612b946d5d456611 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 14:37:51 -0500 Subject: [PATCH 23/24] fix: tighten private DNS project API bounds and admission checks DNSNamingPolicy now carries the Project parent-context discovery annotation like the other project-scoped kinds, so it appears in project-context discovery. Contribution reasons are capped at 1024 characters, and naming policy scope and name prefixes at 253, the length of a DNS name. Contribution record sets become a map keyed by record type, so each type appears in one set only. The admission webhook now rejects contribution owners outside the registration's name and reserved descendants, mirroring the publisher's NameOrTypeOutsideRegistration rule so writers get the error at write time. Associations now fetch their resolver context and reject a missing, replaced or deleting one. They do not require Ready, because the context controller creates its managed zone association before the context can become Ready. Co-Authored-By: Claude Opus 5.5 --- api/v1alpha1/internaldns_project_types.go | 6 +++ ...orking.miloapis.com_dnsnamingpolicies.yaml | 3 ++ ...g.miloapis.com_dnsrecordcontributions.yaml | 4 ++ internal/internaldns/admission/handler.go | 43 +++++++++++++++- .../internaldns/admission/handler_test.go | 50 +++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 6a30378a..79fb2608 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -207,8 +207,11 @@ type DNSContributionRecordSet struct { type DNSRecordContributionSpec struct { RegistrationRef DNSObjectReference `json:"registrationRef"` // +kubebuilder:validation:MaxItems=14 + // +listType=map + // +listMapKey=recordType RecordSets []DNSContributionRecordSet `json:"recordSets"` Eligible bool `json:"eligible"` + // +kubebuilder:validation:MaxLength=1024 Reason string `json:"reason,omitempty"` } @@ -245,6 +248,7 @@ type DNSNamingPolicySpec struct { // +kubebuilder:validation:MaxItems=256 AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` + // +kubebuilder:validation:MaxLength=253 ScopePrefix string `json:"scopePrefix,omitempty"` Priority int32 `json:"priority,omitempty"` } @@ -253,6 +257,7 @@ type DNSNamingPolicySpec struct { type DNSAdditionalNameRule struct { Selector *DNSRegistrationSelector `json:"selector,omitempty"` DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` + // +kubebuilder:validation:MaxLength=253 NamePrefix string `json:"namePrefix"` } @@ -306,6 +311,7 @@ type DNSResolvedAdditionalNameRule struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:metadata:annotations="discovery.miloapis.com/parent-contexts=Project" type DNSNamingPolicy struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml index f25d5887..6da49ceb 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsnamingpolicies.yaml @@ -4,6 +4,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.19.0 + discovery.miloapis.com/parent-contexts: Project name: dnsnamingpolicies.dns.networking.miloapis.com spec: group: dns.networking.miloapis.com @@ -63,6 +64,7 @@ spec: - name type: object namePrefix: + maxLength: 253 type: string selector: description: DNSRegistrationSelector matches registration labels @@ -166,6 +168,7 @@ spec: - name type: object scopePrefix: + maxLength: 253 type: string required: - additionalNames diff --git a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml index 27571e98..766cbde6 100644 --- a/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_dnsrecordcontributions.yaml @@ -41,6 +41,7 @@ spec: eligible: type: boolean reason: + maxLength: 1024 type: string recordSets: items: @@ -287,6 +288,9 @@ spec: type: object maxItems: 14 type: array + x-kubernetes-list-map-keys: + - recordType + x-kubernetes-list-type: map registrationRef: description: |- DNSObjectReference pins a namespaced object by API-assigned UID. Generation diff --git a/internal/internaldns/admission/handler.go b/internal/internaldns/admission/handler.go index 5a1d5faf..a1cb691a 100644 --- a/internal/internaldns/admission/handler.go +++ b/internal/internaldns/admission/handler.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "slices" + "strings" dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" "go.miloapis.com/dns-operator/internal/internaldns/model" @@ -137,11 +138,40 @@ func (h *Handler) contribution(ctx context.Context, cl client.Client, req kubead if err := model.ValidatePrivateRecordOwner(record.Name, zone.Spec.DomainName, string(set.RecordType)); err != nil { return err } + if !registrationCoversOwner(®istration, record.Name, zone.Spec.DomainName) { + return fmt.Errorf("record owner %q is outside the registration", record.Name) + } } } return nil } +// registrationCoversOwner mirrors the publisher's name scope so writers get +// immediate feedback: an owner must be the registered name or fall within one +// of its reserved descendants. +func registrationCoversOwner(registration *dnsv1alpha1.DNSRegistration, owner, apex string) bool { + name := zoneRelativeOwner(owner, apex) + if name == zoneRelativeOwner(registration.Spec.Name, apex) { + return true + } + for _, scope := range registration.Spec.ReservedDescendants { + s := zoneRelativeOwner(scope, apex) + if name == s || strings.HasSuffix(name, "."+s) { + return true + } + } + return false +} + +func zoneRelativeOwner(name, apex string) string { + n := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(name), ".")) + a := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(apex), ".")) + if n == a { + return "@" + } + return strings.TrimSuffix(n, "."+a) +} + 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 { @@ -151,8 +181,17 @@ func (h *Handler) association(ctx context.Context, cl client.Client, req kubeadm return err } - if a.Spec.ResolverContextRef.UID == "" { - return fmt.Errorf("association must pin resolver context UID") + if a.Spec.ResolverContextRef.Name == "" || a.Spec.ResolverContextRef.UID == "" { + return fmt.Errorf("association must pin resolver context name and UID") + } + // Readiness is not required here: the context controller creates the + // managed zone association before the context itself can become Ready. + var resolverContext dnsv1alpha1.DNSResolverContext + if err := cl.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: a.Spec.ResolverContextRef.Name}, &resolverContext); err != nil { + return fmt.Errorf("resolver context lookup: %w", err) + } + if resolverContext.UID != a.Spec.ResolverContextRef.UID || !resolverContext.DeletionTimestamp.IsZero() { + return fmt.Errorf("resolver context lifetime is not current") } return nil } diff --git a/internal/internaldns/admission/handler_test.go b/internal/internaldns/admission/handler_test.go index e0151027..d709af52 100644 --- a/internal/internaldns/admission/handler_test.go +++ b/internal/internaldns/admission/handler_test.go @@ -177,6 +177,56 @@ func TestContributionValidatesReferencesForRBACAuthorizedCallers(t *testing.T) { } } +func TestContributionOwnersStayWithinRegistration(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", ReservedDescendants: []string{"targets.web"}, RecordTypes: []dnsv1alpha1.RRType{dnsv1alpha1.RRTypeA}}} + handler := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone, registration).Build()} + for _, tt := range []struct { + owner string + allowed bool + }{{"web", true}, {"WEB.datum.internal.", true}, {"targets.web", true}, {"a.targets.web.datum.internal", true}, {"api", false}, {"other.web", false}, {"@", false}} { + 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: tt.owner, A: &dnsv1alpha1.ARecordSpec{Content: "10.0.0.8"}}}}}}} + if got := handler.Handle(context.Background(), request("dnsrecordcontributions", "publisher", contribution, nil)); got.Allowed != tt.allowed { + t.Errorf("owner %q: allowed=%v expected=%v: %v", tt.owner, got.Allowed, tt.allowed, got.Result) + } + } +} + +func TestAssociationRequiresCurrentResolverContext(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}} + // A context that is not yet Ready is still a valid association target. + resolverContext := &dnsv1alpha1.DNSResolverContext{ObjectMeta: metav1.ObjectMeta{Name: "ctx", Namespace: "project", UID: "ctx-uid", Generation: 1}} + association := func(contextUID types.UID) *dnsv1alpha1.DNSZoneAssociation { + return &dnsv1alpha1.DNSZoneAssociation{Spec: dnsv1alpha1.DNSZoneAssociationSpec{DNSZoneRef: dnsv1alpha1.DNSObjectReference{Name: zone.Name, UID: zone.UID}, ResolverContextRef: dnsv1alpha1.DNSObjectReference{Name: resolverContext.Name, UID: contextUID}}} + } + h := Handler{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone, resolverContext).Build()} + if got := h.Handle(context.Background(), request("dnszoneassociations", "tenant", association("ctx-uid"), nil)); !got.Allowed { + t.Fatalf("current context rejected: %v", got.Result) + } + if h.Handle(context.Background(), request("dnszoneassociations", "tenant", association("replaced"), nil)).Allowed { + t.Fatal("association accepted a replaced resolver context") + } + h.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone).Build() + if h.Handle(context.Background(), request("dnszoneassociations", "tenant", association("ctx-uid"), nil)).Allowed { + t.Fatal("association accepted a missing resolver context") + } + deleting := resolverContext.DeepCopy() + deleting.Finalizers = []string{"test"} + deleting.DeletionTimestamp = &metav1.Time{Time: time.Now()} + h.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(zone, deleting).Build() + if h.Handle(context.Background(), request("dnszoneassociations", "tenant", association("ctx-uid"), nil)).Allowed { + t.Fatal("association accepted a deleting resolver context") + } +} + func TestDeletingResourceCanRemoveFinalizersWithoutReferences(t *testing.T) { h := Handler{} for _, resource := range []string{"dnsregistrations", "dnsrecordcontributions", "dnszoneassociations", "dnsnamingpolicies", "dnsresolveraccessbindings"} { From 1b642a7f430cec67269c41a4e094ce738ccc08d0 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 22 Oct 2026 14:39:25 -0500 Subject: [PATCH 24/24] style: gofmt the private DNS project API fields Co-Authored-By: Claude Opus 5.5 --- api/v1alpha1/internaldns_project_types.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/internaldns_project_types.go b/api/v1alpha1/internaldns_project_types.go index 79fb2608..5ffcb4a0 100644 --- a/api/v1alpha1/internaldns_project_types.go +++ b/api/v1alpha1/internaldns_project_types.go @@ -212,7 +212,7 @@ type DNSRecordContributionSpec struct { RecordSets []DNSContributionRecordSet `json:"recordSets"` Eligible bool `json:"eligible"` // +kubebuilder:validation:MaxLength=1024 - Reason string `json:"reason,omitempty"` + Reason string `json:"reason,omitempty"` } type DNSRecordContributionStatus struct { @@ -249,8 +249,8 @@ type DNSNamingPolicySpec struct { AdditionalNames []DNSAdditionalNameRule `json:"additionalNames"` DNSZoneRef DNSObjectReference `json:"dnsZoneRef,omitempty"` // +kubebuilder:validation:MaxLength=253 - ScopePrefix string `json:"scopePrefix,omitempty"` - Priority int32 `json:"priority,omitempty"` + ScopePrefix string `json:"scopePrefix,omitempty"` + Priority int32 `json:"priority,omitempty"` } // +kubebuilder:validation:XValidation:rule="has(self.dnsZoneRef.uid) && self.dnsZoneRef.uid.size() > 0",message="dnsZoneRef must pin the zone UID" @@ -258,7 +258,7 @@ type DNSAdditionalNameRule struct { Selector *DNSRegistrationSelector `json:"selector,omitempty"` DNSZoneRef DNSObjectReference `json:"dnsZoneRef"` // +kubebuilder:validation:MaxLength=253 - NamePrefix string `json:"namePrefix"` + NamePrefix string `json:"namePrefix"` } // DNSRegistrationSelector matches registration labels using Kubernetes selector semantics.