Skip to content
Open
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
1 change: 1 addition & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ tasks:
cmds:
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-cni ./cmd/galactic-cni
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-router ./cmd/galactic-router
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-webhook ./cmd/galactic-webhook
- go build -ldflags "{{.LDFLAGS}}" -o bin/vmtap-cni ./cmd/vmtap-cni
- GOBIN={{.LOCALBIN}} go install github.com/containernetworking/plugins/plugins/main/host-device@v1.9.1

Expand Down
18 changes: 18 additions & 0 deletions cmd/galactic-webhook/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2025 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

// Command galactic-webhook is the mutating admission webhook that provisions
// NAD + VPCAttachment ID allocation for pods requesting VPC attachment. See
// internal/webhook for the handler logic and this repo's design plan
// (.local/plan-vpc-nad-webhook-plan.md) for the full rationale.
package main

import "os"

func main() {
cmd := newRootCommand()
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
127 changes: 127 additions & 0 deletions cmd/galactic-webhook/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2025 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/spf13/cobra"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"go.datum.net/galactic/internal/config"
"go.datum.net/galactic/internal/metadata"
galacticwebhook "go.datum.net/galactic/internal/webhook"
)

const (
appName = "galactic-webhook"

appDesc = `Galactic VPC-attachment mutating admission webhook

Find more information at: https://www.datum.net/docs`

// mutatePodPath is the path the MutatingWebhookConfiguration in
// config/webhook/ targets.
mutatePodPath = "/mutate-v1-pod-vpc-attachment"
)

// runCmd contains the application startup logic.
func runCmd(cfg *config.WebhookConfig) error {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

scheme := galacticwebhook.NewScheme()

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: fmt.Sprintf(":%d", cfg.MetricsPort),
},
HealthProbeBindAddress: fmt.Sprintf(":%d", cfg.HealthPort),
WebhookServer: webhook.NewServer(webhook.Options{
Port: cfg.Port,
CertDir: cfg.CertDir,
}),
})
if err != nil {
return fmt.Errorf("create manager: %w", err)
}

// Cache-sync-gated readiness: a cold cache means the ID allocator can't
// see existing NADs and would double-allocate IDs — see this repo's
// design plan, "Client: cache-backed reads, direct writes."
if err := mgr.AddReadyzCheck("informer-sync", func(req *http.Request) error {
if !mgr.GetCache().WaitForCacheSync(req.Context()) {
return errors.New("informer cache not yet synced")
}
return nil
}); err != nil {
return fmt.Errorf("add readyz check: %w", err)
}
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return fmt.Errorf("add healthz check: %w", err)
}

mutator := &galacticwebhook.PodMutator{
Client: mgr.GetClient(),
Decoder: admission.NewDecoder(scheme),
NADDefaults: galacticwebhook.NADDefaults{
MTU: cfg.MTU,
InterfaceType: cfg.InterfaceType,
},
}
mgr.GetWebhookServer().Register(mutatePodPath, &webhook.Admission{Handler: mutator})

ctx := ctrl.SetupSignalHandler()
if err := mgr.Start(ctx); err != nil {
return fmt.Errorf("manager exited: %w", err)
}
return nil
}

// newRootCommand builds the root cobra command with all flags and the
// application startup logic.
func newRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: appName,
Short: strings.Split(appDesc, "\n")[0],
Long: appDesc,
RunE: func(cmd *cobra.Command, _ []string) error {
if ok, _ := cmd.Flags().GetBool("build-info"); ok {
fmt.Println(metadata.BuildInfo(appName))
return nil
}
if ok, _ := cmd.Flags().GetBool("version"); ok {
fmt.Printf("galactic-webhook version %s\n", metadata.Version)
return nil
}

cfg := config.NewWebhookConfig()
cfg.BindFlags(cmd.Flags())
if err := cfg.Validate(); err != nil {
return err
}
return runCmd(cfg)
},
}

cmd.Flags().IntP("port", "p", config.DefaultWebhookPort, "Webhook server listen port")
cmd.Flags().IntP("metrics-port", "", config.DefaultWebhookMetricsPort, "Metrics listen port")
cmd.Flags().IntP("health-port", "", config.DefaultWebhookHealthPort, "Health/readiness probe listen port")
cmd.Flags().StringP("cert-dir", "", config.DefaultWebhookCertDir, "Directory containing tls.crt/tls.key")
cmd.Flags().IntP("mtu", "", config.DefaultWebhookMTU, "MTU baked into every NAD's CNI conflist")
cmd.Flags().StringP("interface-type", "", config.DefaultWebhookInterfaceType,
"Interface type baked into every NAD's CNI conflist (\"veth\" or \"tap\")")
cmd.Flags().Bool("build-info", false, "Print build information and exit")
cmd.Flags().BoolP("version", "V", false, "Print version and exit")
return cmd
}
11 changes: 11 additions & 0 deletions config/cni/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ rules:
resources:
- network-attachment-definitions
verbs: ["patch"]
# vpcattachments: galactic-cni creates the VPCAttachment CR and writes its
# status at ADD time (internal/cni's applyVPCAttachment), at the same
# point it creates BGPVRFInstance/BGPAdvertisement above.
- apiGroups: ["cloud.datumapis.com"]
resources:
- vpcattachments
verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: ["cloud.datumapis.com"]
resources:
- vpcattachments/status
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources:
- nodes
Expand Down
1 change: 1 addition & 0 deletions config/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ resources:
- cni
- router
- vmtap
- webhook
24 changes: 24 additions & 0 deletions config/router/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ rules:
resources:
- nodes
verbs: ["get", "list", "watch"]
# pods (cluster-wide, all namespaces): the GC reconciler checks pod
# existence to reclaim orphaned VPCAttachments (Status.PodName) and NADs
# (referenced via the Multus networks annotation) — see internal/gc's
# CollectOrphanedVPCAttachments/CollectOrphanedNADs.
- apiGroups: [""]
resources:
- pods
verbs: ["get", "list", "watch"]
# vpcattachments (cluster-wide): galactic-cni (a different ServiceAccount,
# see config/cni/rbac.yaml) creates these and writes their status at ADD
# time. galactic-router's GC reconciler only reads and deletes orphaned
# ones — no create/update/status verbs needed here.
- apiGroups: ["cloud.datumapis.com"]
resources:
- vpcattachments
verbs: ["get", "list", "watch", "delete"]
# network-attachment-definitions (cluster-wide): galactic-webhook (a
# different ServiceAccount, see config/webhook/rbac.yaml) creates these;
# galactic-cni patches the host-interface annotation onto them. GC only
# reads and deletes orphaned ones.
- apiGroups: ["k8s.cni.cncf.io"]
resources:
- network-attachment-definitions
verbs: ["get", "list", "watch", "delete"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Expand Down
28 changes: 28 additions & 0 deletions config/webhook/certificate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Self-signed CA + serving certificate for the webhook's TLS listener,
# mirroring the minimal cert-manager setup common for controller-runtime
# webhooks: a self-signed Issuer bootstraps a CA, which then issues the
# actual serving Certificate. The MutatingWebhookConfiguration's
# cert-manager.io/inject-ca-from annotation (see
# mutatingwebhookconfiguration.yaml) has cert-manager auto-populate
# caBundle from this Certificate — no manual CA distribution needed.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: galactic-webhook-selfsigned
namespace: galactic-system
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: galactic-webhook-serving-cert
namespace: galactic-system
spec:
secretName: galactic-webhook-serving-certs
issuerRef:
name: galactic-webhook-selfsigned
kind: Issuer
dnsNames:
- galactic-webhook.galactic-system.svc
- galactic-webhook.galactic-system.svc.cluster.local
74 changes: 74 additions & 0 deletions config/webhook/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: galactic-webhook
namespace: galactic-system
labels:
app.kubernetes.io/name: galactic-webhook
spec:
# 2+ replicas: failurePolicy: Fail on the MutatingWebhookConfiguration
# makes webhook unavailability a pod-admission outage — see this repo's
# design plan, Component placement.
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: galactic-webhook
template:
metadata:
labels:
app.kubernetes.io/name: galactic-webhook
spec:
serviceAccountName: galactic-webhook
containers:
- name: galactic-webhook
# ":latest" is a placeholder only: no such tag is ever pushed to
# GHCR. CI resolves and stamps a real version tag here when it
# publishes the ghcr.io/datum-cloud/galactic-kustomize OCI bundle
# (see .github/workflows/publish.yaml); applying this manifest
# directly from a git checkout will fail to pull the image unless
# you override the tag first — see the README's "Production
# Deployment" section.
image: ghcr.io/datum-cloud/galactic-webhook:latest
command:
- /galactic-webhook
ports:
- name: webhook
containerPort: 9443
protocol: TCP
- name: metrics
containerPort: 8080
protocol: TCP
- name: health
containerPort: 8081
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
volumeMounts:
- name: serving-certs
mountPath: /etc/webhook/certs
readOnly: true
volumes:
- name: serving-certs
secret:
secretName: galactic-webhook-serving-certs
7 changes: 7 additions & 0 deletions config/webhook/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resources:
- serviceaccount.yaml
- rbac.yaml
- deployment.yaml
- service.yaml
- certificate.yaml
- mutatingwebhookconfiguration.yaml
35 changes: 35 additions & 0 deletions config/webhook/mutatingwebhookconfiguration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: galactic-webhook
annotations:
# Has cert-manager auto-populate every webhook's clientConfig.caBundle
# below from this Certificate's CA — see certificate.yaml.
cert-manager.io/inject-ca-from: galactic-system/galactic-webhook-serving-cert
webhooks:
- name: vpc-attachment-injector.galactic.datumapis.com
clientConfig:
service:
name: galactic-webhook
namespace: galactic-system
path: /mutate-v1-pod-vpc-attachment
port: 443
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE"]
failurePolicy: Fail
sideEffects: NoneOnDryRun
matchPolicy: Equivalent
reinvocationPolicy: IfNeeded
admissionReviewVersions: ["v1"]
timeoutSeconds: 10
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- kube-system
- kube-node-lease
- galactic-system
32 changes: 32 additions & 0 deletions config/webhook/rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: galactic-webhook
rules:
# vpcs (cluster-wide): a pod's vpc-attachment annotation names a VPC in
# the pod's own namespace, which can be any namespace — see this repo's
# design plan, Handler logic step 5.
- apiGroups: ["cloud.datumapis.com"]
resources:
- vpcs
verbs: ["get", "list", "watch"]
# network-attachment-definitions (cluster-wide): the webhook creates one
# NAD per pod attachment and lists existing ones (labelVPC) to allocate
# a free VPCAttachment ID — see internal/webhook/allocate.go.
- apiGroups: ["k8s.cni.cncf.io"]
resources:
- network-attachment-definitions
verbs: ["get", "list", "watch", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: galactic-webhook
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: galactic-webhook
subjects:
- kind: ServiceAccount
name: galactic-webhook
namespace: galactic-system
Loading