Skip to content
Merged
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
128 changes: 128 additions & 0 deletions api/v1alpha1/serviceroute_admission_envtest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2026 Datum Cloud, Inc.
// SPDX-License-Identifier: AGPL-3.0-or-later

package v1alpha1_test

import (
"context"
"os"
"path/filepath"
"testing"
"time"

corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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"
"sigs.k8s.io/yaml"

networkv1alpha1 "go.datum.net/network/api/v1alpha1"
)

func TestTranslatedAPIAdmission(t *testing.T) {
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
t.Skip("set KUBEBUILDER_ASSETS for isolated API admission tests")
}
apiRoot := os.Getenv("GALACTIC_TEST_NETWORK_API")
if apiRoot == "" {
apiRoot = "../.."
}
crds := make([]*apiextensionsv1.CustomResourceDefinition, 0, 2)
for _, name := range []string{
"network.datumapis.com_serviceendpoints.yaml",
"network.datumapis.com_serviceroutepolicies.yaml",
} {
data, err := os.ReadFile(filepath.Join(apiRoot, "config", "crd", name))
if err != nil {
t.Fatal(err)
}
crd := &apiextensionsv1.CustomResourceDefinition{}
if err := yaml.Unmarshal(data, crd); err != nil {
t.Fatal(err)
}
crds = append(crds, crd)
}
environment := &envtest.Environment{CRDs: crds}
cfg, err := environment.Start()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := environment.Stop(); err != nil {
t.Errorf("stop API: %v", err)
}
})
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = networkv1alpha1.AddToScheme(scheme)
c, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
if err := c.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "psc-admission"}}); err != nil {
t.Fatal(err)
}
policy := func(name string) *networkv1alpha1.ServiceRoutePolicy {
return &networkv1alpha1.ServiceRoutePolicy{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "psc-admission"},
Spec: networkv1alpha1.ServiceRoutePolicySpec{
ServiceRef: networkv1alpha1.ServiceEndpointReference{Name: "dns"},
AttachmentSelector: metav1.LabelSelector{MatchLabels: map[string]string{"access": "dns"}},
Frontend: &networkv1alpha1.ServiceRouteFrontend{Address: "fd53::53"},
ConsumerVPCRef: &networkv1alpha1.ServiceRouteVPCReference{Name: "vpc", UID: "vpc-uid"},
Authorization: &networkv1alpha1.ServiceRouteAuthorization{
ValidUntil: metav1.NewTime(time.Now().Add(time.Minute)),
},
},
}
}
for _, tt := range []struct {
name string
mutate func(*networkv1alpha1.ServiceRoutePolicy)
wantError bool
}{
{"missing-pin", func(p *networkv1alpha1.ServiceRoutePolicy) { p.Spec.ConsumerVPCRef = nil }, true},
{"empty-uid", func(p *networkv1alpha1.ServiceRoutePolicy) { p.Spec.ConsumerVPCRef.UID = "" }, true},
{"missing-authorization", func(p *networkv1alpha1.ServiceRoutePolicy) { p.Spec.Authorization = nil }, true},
{"legacy-direct", func(p *networkv1alpha1.ServiceRoutePolicy) {
p.Spec.Frontend = nil
p.Spec.ConsumerVPCRef = nil
p.Spec.Authorization = nil
}, false},
{"translated", func(*networkv1alpha1.ServiceRoutePolicy) {}, false},
} {
t.Run(tt.name, func(t *testing.T) {
p := policy(tt.name)
tt.mutate(p)
err := c.Create(ctx, p)
if (err != nil) != tt.wantError {
t.Fatalf("create error=%v", err)
}
if tt.wantError && !apierrors.IsInvalid(err) {
t.Fatalf("expected schema rejection, got %v", err)
}
})
}
// A real API preserves competing node reports and prevents stale resource-
// version writes from replacing a newer authorization generation.
p := policy("fenced")
if err := c.Create(ctx, p); err != nil {
t.Fatal(err)
}
old := p.DeepCopy()
p.Spec.Authorization.ValidUntil = metav1.NewTime(time.Now().Add(90 * time.Second))
if err := c.Update(ctx, p); err != nil {
t.Fatal(err)
}
old.Status.Nodes = []networkv1alpha1.ServiceRouteNodeStatus{{
NodeName: "node-a", PolicyUID: string(old.UID), ObservedGeneration: old.Generation,
InputDigest: "old", Ready: true, ValidUntil: metav1.NewTime(time.Now().Add(time.Minute)),
}}
if err := c.Status().Update(ctx, old); !apierrors.IsConflict(err) {
t.Fatalf("stale status write error=%v", err)
}
}
65 changes: 63 additions & 2 deletions api/v1alpha1/serviceroute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,30 @@ type ServiceRoutePolicy struct {
}

// ServiceRoutePolicySpec defines service selection and attachment eligibility.
// +kubebuilder:validation:XValidation:rule="!has(self.frontend) || has(self.consumerVPCRef)",message="frontend requires consumerVPCRef"
// +kubebuilder:validation:XValidation:rule="!has(self.frontend) || has(self.authorization)",message="frontend requires authorization"
type ServiceRoutePolicySpec struct {
// ServiceRef identifies the ServiceEndpoint in this namespace.
// +kubebuilder:validation:Required
ServiceRef ServiceEndpointReference `json:"serviceRef"`

// ConsumerVPCRef pins authorization to the immutable lifetime of the VPC
// selected by AttachmentSelector. Galactic resolves the name through the
// Cloud API and requires both the live name and UID to match.
// +optional
ConsumerVPCRef *ServiceRouteVPCReference `json:"consumerVPCRef,omitempty"`

// Frontend is the address presented inside the consumer VPC. When set,
// Galactic translates it to the ServiceEndpoint address and restores it on
// replies.
// +optional
Frontend *ServiceRouteFrontend `json:"frontend,omitempty"`

// Authorization bounds translated access independently of application leases.
// Only trusted networking integration may renew this deadline.
// +optional
Authorization *ServiceRouteAuthorization `json:"authorization,omitempty"`

// AttachmentSelector selects eligible VPC attachments by authoritative
// labels. Labels that grant access to platform services must be assigned by
// the network control plane, not by tenants.
Expand All @@ -149,6 +168,32 @@ type ServiceRoutePolicySpec struct {
Region string `json:"region,omitempty"`
}

// ServiceRouteAuthorization bounds one network authorization episode.
type ServiceRouteAuthorization struct {
// ValidUntil is the absolute UTC deadline for requests and existing replies.
// Node controllers reject deadlines more than two minutes in the future.
ValidUntil metav1.Time `json:"validUntil"`
}

// ServiceRouteVPCReference identifies one immutable Cloud API VPC lifetime.
type ServiceRouteVPCReference struct {
// Name is the VPC name in the policy namespace.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// UID is the Kubernetes UID of that VPC lifetime.
// +kubebuilder:validation:MinLength=1
UID string `json:"uid"`
}

// ServiceRouteFrontend declares the consumer-facing service address.
type ServiceRouteFrontend struct {
// Address is translated to the ServiceEndpoint address for requests and is
// restored as the source address for replies.
// +kubebuilder:validation:XValidation:rule="isIP(self)",message="address must be an IP address"
Address string `json:"address"`
}

// ServiceRouteProtocolPort identifies an allowed transport protocol and port.
type ServiceRouteProtocolPort struct {
// Protocol is the transport protocol.
Expand All @@ -170,8 +215,7 @@ type ServiceEndpointReference struct {
Name string `json:"name"`
}

// ServiceRoutePolicyStatus contains durable policy conditions only. Per-
// attachment selection and programming state belongs in metrics and logs.
// ServiceRoutePolicyStatus contains policy conditions and fenced node reports.
type ServiceRoutePolicyStatus struct {
// ObservedGeneration is the .metadata.generation this status was computed from.
// +optional
Expand All @@ -183,6 +227,23 @@ type ServiceRoutePolicyStatus struct {
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`

// Nodes contains short-lived acknowledgments written by trusted node controllers.
// Consumers require current UID, generation, input digest, and unexpired reports.
// +listType=map
// +listMapKey=nodeName
// +optional
Nodes []ServiceRouteNodeStatus `json:"nodes,omitempty"`
}

// ServiceRouteNodeStatus acknowledges one node's current programmed path.
type ServiceRouteNodeStatus struct {
NodeName string `json:"nodeName"`
PolicyUID string `json:"policyUID"`
ObservedGeneration int64 `json:"observedGeneration"`
InputDigest string `json:"inputDigest"`
Ready bool `json:"ready"`
ValidUntil metav1.Time `json:"validUntil"`
}

const (
Expand Down
84 changes: 84 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Loading
Loading