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
87 changes: 87 additions & 0 deletions cmd/galactic-vrf/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2026 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

// Command galactic-vrf is #855's ingress sidecar: the second container in
// the shared Envoy Gateway fleet's pod, responsible only for VPC backend
// connectivity — Linux VRF device + SRv6 seg6 encap route lifecycle, driven
// entirely by a cluster-scoped watch on discoveryv1.EndpointSlice objects
// galactic-cni (#854) publishes per pod. See
// docs/plans/855-ingress-sidecar-vpc-backend-connectivity.md and
// internal/ingresssidecar's own doc comment for the full design; this
// binary is just the process wiring (config, manager, metrics, RBAC
// pre-flight) around that package.
//
// Unlike galactic-router/galactic-gateway, this binary exposes no gRPC
// health server: neither of this repo's existing binaries has an
// established /healthz convention to copy (galactic-router explicitly
// disables its health probe; galactic-cni has none at all — see §5 of the
// plan), so building one here would be new design work rather than
// following a pattern, and container liveness (process exits, kubelet
// restarts it) is enough for v1.
package main

import (
"context"
"os"
"time"

authorizationv1 "k8s.io/api/authorization/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
discoveryAPIGroup = "discovery.k8s.io"
discoveryAPIVersion = "v1"
resourceEndpointSlices = "endpointslices"
)

func main() {
cmd := newRootCommand()
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

// checkWatchPermissions issues a SelfSubjectAccessReview for the "watch"
// verb on endpointslices, the only resource this binary's manager watches.
// If the review denies the request the informer cache will never sync and
// the reconciler will be silently blocked; this logs a clear, actionable
// message at startup so the problem is immediately obvious. Mirrors
// cmd/galactic-router and cmd/galactic-gateway's identically-named
// functions, scoped to this binary's own single resource — see §9 item 8
// of the plan (the read-only ClusterRole decision this check exists to
// help catch a misconfiguration of).
func checkWatchPermissions(mgr ctrl.Manager) {
c, err := client.New(mgr.GetConfig(), client.Options{Scheme: mgr.GetScheme()})
if err != nil {
ctrl.Log.Error(err, "RBAC pre-flight: cannot create client, skipping check")
return
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

logger := ctrl.Log.WithName("rbac-preflight")

review := &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Verb: "watch",
Group: discoveryAPIGroup,
Version: discoveryAPIVersion,
Resource: resourceEndpointSlices,
},
},
}
if err := c.Create(ctx, review); err != nil {
logger.Error(err, "RBAC pre-flight: failed to submit access review for "+resourceEndpointSlices, "verb", "watch")
return
}
if review.Status.Allowed {
return
}
logger.Error(nil, "missing watch RBAC for "+resourceEndpointSlices,
"verb", "watch", "detail", "informer cache will not sync; add resource to ServiceAccount ClusterRole and restart")
}
151 changes: 151 additions & 0 deletions cmd/galactic-vrf/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2026 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

"go.datum.net/galactic/internal/config"
"go.datum.net/galactic/internal/ingresssidecar"
"go.datum.net/galactic/internal/metadata"
)

const (
appName = "galactic-vrf"

appDesc = `Galactic ingress sidecar: per-pod VPC backend VRF/SRv6 route lifecycle

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

// runCmd contains the application startup logic: it registers
// internal/ingresssidecar's Reconciler against a cluster-scoped
// EndpointSlice watch, then seeds Store from the live API state and runs
// its startup inventory and periodic sweep. There is no BGP runtime, CRD
// scheme beyond clientgoscheme's built-in discoveryv1 registration, or
// per-node identity of any kind here — see internal/config.VRFConfig's own
// doc comment for why.
func runCmd(cfg *config.VRFConfig) error {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))

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

ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
defer cancel()

// Pre-flight RBAC check.
checkWatchPermissions(mgr)

metrics := ingresssidecar.NewMetrics()
metrics.MustRegister(ctrlmetrics.Registry)

backend := ingresssidecar.NewKernelBackend()
store := ingresssidecar.NewStore(backend, cfg.TeardownGracePeriod, metrics)

if err := (&ingresssidecar.Reconciler{Store: store}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setup EndpointSlice controller: %w", err)
}

// Startup seed + inventory + periodic sweep. Every EndpointSlice that
// exists at boot must be visible to Store *before* Inventory or Sweep
// ever run, or a live VPC/pod could be misjudged as orphaned -- see
// ingresssidecar.SeedFromAPI's own doc comment for why that can no
// longer be mgr.GetCache().WaitForCacheSync's job: a synced cache only
// guarantees the informer's initial List landed in the cache, not that
// the controller's own Reconcile has drained the workqueue that same
// List fed, so on a busy node at boot the two could race. SeedFromAPI
// uses mgr.GetAPIReader(), the manager's uncached reader, so it
// doesn't depend on cache/workqueue timing at all. Mirrors
// cmd/galactic-router's own GC-ticker startup goroutine.
go func() {
if err := ingresssidecar.SeedFromAPI(ctx, mgr.GetAPIReader(), store); err != nil {
log.Printf("startup seed: %v", err)
return
}
if err := store.Inventory(ctx, time.Now()); err != nil {
log.Printf("startup inventory: %v", err)
}
ingresssidecar.RunSweeper(ctx, store, cfg.SweepInterval)
}()

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

// mgr.Start only returns nil once ctx is Done (signal-triggered
// shutdown -- there's no other source of cancellation here, unlike
// cmd/galactic-router/cmd/galactic-gateway's health-server-failure
// case). No proactive VRF/route teardown on exit: §6 of the plan
// leans toward leaving kernel state for the next instance to
// reconcile from scratch, since a live Envoy container next to a
// dying sidecar mid-rollout would otherwise blackhole in-flight
// connections.
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-vrf version %s\n", metadata.Version)
return nil
}

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

cmd.Flags().IntP("metrics-port", "",
config.DefaultVRFMetricsPort,
"Metrics listen port")
cmd.Flags().DurationP("teardown-grace-period", "",
config.DefaultVRFTeardownGracePeriod,
"Delay before tearing down a route/VRF after it drops out of desired state")
cmd.Flags().DurationP("sweep-interval", "",
config.DefaultVRFSweepInterval,
"How often to re-check pending teardowns")
cmd.Flags().Bool("build-info", false, "Print build information and exit")
cmd.Flags().BoolP("version", "V", false, "Print version and exit")
return cmd
}
65 changes: 65 additions & 0 deletions cmd/galactic-vrf/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
"testing"

"github.com/spf13/cobra"

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

// testCmd creates a cobra command with the same flags as newRootCommand.
func testCmd(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "test"}
cmd.Flags().IntP("metrics-port", "", config.DefaultVRFMetricsPort, "Metrics listen port")
cmd.Flags().DurationP("teardown-grace-period", "", config.DefaultVRFTeardownGracePeriod, "Teardown grace period")
cmd.Flags().DurationP("sweep-interval", "", config.DefaultVRFSweepInterval, "Sweep interval")
return cmd
}

func TestFlagDefaults(t *testing.T) {
cfg := config.NewVRFConfig()
cmd := testCmd(t)
cfg.BindFlags(cmd.Flags())

if cfg.MetricsPort != config.DefaultVRFMetricsPort {
t.Errorf("MetricsPort = %d, want %d", cfg.MetricsPort, config.DefaultVRFMetricsPort)
}
if cfg.TeardownGracePeriod != config.DefaultVRFTeardownGracePeriod {
t.Errorf("TeardownGracePeriod = %v, want %v", cfg.TeardownGracePeriod, config.DefaultVRFTeardownGracePeriod)
}
if cfg.SweepInterval != config.DefaultVRFSweepInterval {
t.Errorf("SweepInterval = %v, want %v", cfg.SweepInterval, config.DefaultVRFSweepInterval)
}
if err := cfg.Validate(); err != nil {
t.Errorf("Validate() with defaults: %v", err)
}
}

func TestFlagsOverrideEnv(t *testing.T) {
t.Setenv(config.EnvVRFMetricsPort, "9000")
t.Setenv(config.EnvVRFTeardownGracePeriod, "60s")

cfg := config.NewVRFConfig()
cmd := testCmd(t)
if err := cmd.Flags().Set("metrics-port", "9500"); err != nil {
t.Fatalf("set --metrics-port flag: %v", err)
}
cfg.BindFlags(cmd.Flags())

if cfg.MetricsPort != 9500 {
t.Errorf("MetricsPort = %d, want 9500 (flag should override env var)", cfg.MetricsPort)
}
}

func TestVersionMetadata(t *testing.T) {
if metadata.Version == "" {
t.Error("metadata.Version should not be empty")
}
}
56 changes: 56 additions & 0 deletions containers/galactic-vrf/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Build galactic-vrf, #855's ingress sidecar. Unlike containers/galactic-cni's
# Dockerfile (one image bundling every CNI-chain binary), this image ships a
# single binary: galactic-vrf runs as its own container, the second one in
# the shared Envoy Gateway fleet's pod (docs/plans/855-ingress-sidecar-vpc-
# backend-connectivity.md §5/§6), not part of the CNI chain at all.
#
# This repo has no wired-up CI publish pipeline for this image yet -- see
# root CLAUDE.md's CI/CD section and §9 item 3 of the plan (the #856
# deployment contract is still an open, flagged dependency) -- this
# Dockerfile exists so the binary is buildable/deployable today, ahead of
# that decision.
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
ARG GIT_COMMIT=unknown
ARG GIT_TREE_STATE=unknown
ARG BUILD_DATE=unknown
ARG SPDX_LICENSE=AGPL-3.0-or-later
ARG GIT_URL=https://github.com/datum-cloud/galactic

WORKDIR /workspace

COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download

COPY cmd/ cmd/
COPY internal/ internal/

RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build \
-ldflags "-s -w \
-X go.datum.net/galactic/internal/metadata.Version=${VERSION} \
-X go.datum.net/galactic/internal/metadata.GitCommit=${GIT_COMMIT} \
-X go.datum.net/galactic/internal/metadata.GitTreeState=${GIT_TREE_STATE} \
-X go.datum.net/galactic/internal/metadata.BuildDate=${BUILD_DATE} \
-X go.datum.net/galactic/internal/metadata.SPDXLicense=${SPDX_LICENSE} \
-X go.datum.net/galactic/internal/metadata.GitURL=${GIT_URL}" \
-o galactic-vrf cmd/galactic-vrf/main.go

# Minimal static-Go-binary image, matching containers/galactic-cni/
# Dockerfile's production-stage pattern -- §6 of the plan. Unlike
# galactic-cni's own final image (alpine, for iproute2/nsenter needed by
# e2e tests), this container never shells out to `ip`: every kernel
# operation goes through internal/plumbing/vrf and internal/plumbing/srv6's
# own netlink calls, so distroless is sufficient.
FROM gcr.io/distroless/static:nonroot

COPY --from=builder /workspace/galactic-vrf /galactic-vrf

# CAP_NET_ADMIN only, never privileged -- per PR #851's explicit privilege
# split (this sidecar creates VRF devices/routes; Envoy only binds to them).
# The capability itself is granted by the pod's securityContext, not here;
# USER is left at distroless:nonroot's default (65532:65532) since
# CAP_NET_ADMIN does not require running as root.
ENTRYPOINT ["/galactic-vrf"]
Loading