Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 11 additions & 29 deletions api/v1alpha1/internaldns_access_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 1 addition & 3 deletions api/v1alpha1/internaldns_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ package v1alpha1
import (
"encoding/json"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestResolverBindingUsesNestedContractAndIndependentCopies(t *testing.T) {
original := &DNSResolverBinding{Spec: DNSResolverBindingSpec{
Source: DNSResolverBindingSource{ProjectUID: "project", ResolverContextRef: DNSObjectReference{Name: "context", UID: "context-uid"}, AccessBindingRef: DNSObjectReference{Name: "access", UID: "access-uid"}},
Placement: DNSResolverBindingPlacement{Region: "central", Shard: "shared"},
Configuration: DNSResolverBindingConfiguration{Generation: 1, Revision: 2, Listeners: DNSResolverBindingListeners{Node: DNSResolverBindingListener{Address: "fd53::1", Port: 53, Transports: []string{"UDP", "TCP"}}, Regional: DNSResolverBindingListener{Address: "fd54::1", Port: 53, Transports: []string{"UDP", "TCP"}}}, ZoneRefs: []DNSObjectReference{{Name: "private", UID: "zone-uid"}}},
Authorization: DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 4, ValidUntil: metav1.Now()},
Authorization: DNSResolverAccessAuthorization{WriterEpoch: 3, Sequence: 4},
}}
copy := original.DeepCopy()
copy.Spec.Configuration.ZoneRefs[0].Name = "changed"
Expand Down
25 changes: 1 addition & 24 deletions api/v1alpha1/internaldns_project_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
package v1alpha1

import (
"encoding/json"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
Expand Down Expand Up @@ -115,24 +112,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"
Expand Down Expand Up @@ -245,8 +225,6 @@ type DNSRegistrationList struct {
}

type DNSProducerPrincipal struct {
// +kubebuilder:validation:MinLength=1
ClusterUID string `json:"clusterUID"`
// +kubebuilder:validation:MinLength=1
Subject string `json:"subject"`
}
Expand Down Expand Up @@ -304,7 +282,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"`
}
Expand Down
12 changes: 5 additions & 7 deletions api/v1alpha1/internaldns_service_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
// DNSResolverBinding is the DNS-service projection of a consumer context and access binding.
// +kubebuilder:validation:XValidation:rule="self.configuration.generation >= oldSelf.configuration.generation && self.configuration.revision >= oldSelf.configuration.revision",message="configuration fences must not decrease"
// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence >= oldSelf.authorization.sequence)",message="authorization fences must not decrease"
// +kubebuilder:validation:XValidation:rule="self.authorization.writerEpoch != oldSelf.authorization.writerEpoch || self.authorization.sequence != oldSelf.authorization.sequence || self.authorization.validUntil == oldSelf.authorization.validUntil",message="changing an authorization deadline requires a new fence"
// +kubebuilder:validation:XValidation:rule="self.configuration.zoneRefs == oldSelf.configuration.zoneRefs || (self.configuration.generation > oldSelf.configuration.generation && self.configuration.revision > oldSelf.configuration.revision)",message="zone membership changes require a new cache generation and configuration revision"
// +kubebuilder:validation:XValidation:rule="(has(self.tombstone) && self.tombstone) == (has(oldSelf.tombstone) && oldSelf.tombstone) || self.configuration.revision > oldSelf.configuration.revision",message="changing withdrawal state requires a new configuration revision"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.tombstone) || !oldSelf.tombstone || (has(self.tombstone) && self.tombstone) || self.authorization.writerEpoch > oldSelf.authorization.writerEpoch || (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch && self.authorization.sequence > oldSelf.authorization.sequence)",message="reactivating withdrawn access requires a new source authorization fence"
Expand Down Expand Up @@ -153,13 +152,12 @@ type DNSRegistrationFence struct {
}

// DNSContributionFence persists an authenticated high-water sequence even
// while its observation is unhealthy, rejected, or expired.
// while its observation is unhealthy or withdrawn.
type DNSContributionFence struct {
UID types.UID `json:"uid"`
GrantUID types.UID `json:"grantUID"`
Epoch int64 `json:"epoch"`
Sequence int64 `json:"sequence"`
ValidUntil metav1.Time `json:"validUntil,omitempty"`
UID types.UID `json:"uid"`
GrantUID types.UID `json:"grantUID"`
Epoch int64 `json:"epoch"`
Sequence int64 `json:"sequence"`
}

type DNSPublicationManifestStatus struct {
Expand Down
9 changes: 2 additions & 7 deletions api/v1alpha1/internaldns_validation_envtest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"testing"
"time"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down
14 changes: 3 additions & 11 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ spec:
items:
description: |-
DNSContributionFence persists an authenticated high-water sequence even
while its observation is unhealthy, rejected, or expired.
while its observation is unhealthy or withdrawn.
properties:
epoch:
format: int64
Expand All @@ -91,9 +91,6 @@ 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
validUntil:
format: date-time
type: string
required:
- epoch
- grantUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,6 @@ spec:
sequence:
format: int64
type: integer
validUntil:
format: date-time
type: string
writerEpoch:
format: int64
type: integer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,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
configuration:
Expand Down Expand Up @@ -263,10 +259,6 @@ spec:
rule: self.authorization.writerEpoch > oldSelf.authorization.writerEpoch
|| (self.authorization.writerEpoch == oldSelf.authorization.writerEpoch
&& self.authorization.sequence >= oldSelf.authorization.sequence)
- message: changing an authorization deadline requires a new fence
rule: self.authorization.writerEpoch != oldSelf.authorization.writerEpoch
|| self.authorization.sequence != oldSelf.authorization.sequence ||
self.authorization.validUntil == oldSelf.authorization.validUntil
- message: zone membership changes require a new cache generation and
configuration revision
rule: self.configuration.zoneRefs == oldSelf.configuration.zoneRefs
Expand Down
Loading
Loading