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
1 change: 1 addition & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ tasks:
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-cni ./cmd/galactic-cni
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-tap-cni ./cmd/galactic-tap-cni
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-ipam ./cmd/galactic-ipam
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-bgp ./cmd/galactic-bgp
- go build -ldflags "{{.LDFLAGS}}" -o bin/galactic-router ./cmd/galactic-router
- 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
91 changes: 91 additions & 0 deletions cmd/galactic-bgp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2026 Datum Cloud, Inc.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
"fmt"
"log"
"os"
"strings"

"github.com/containernetworking/cni/pkg/version"
"github.com/spf13/cobra"
"golang.org/x/term"

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

const (
appName = "galactic-bgp"

appDesc = `Galactic BGP CNI Plugin

The BGP/SRv6/eBPF publish plugin in the galactic CNI chain — chained after
galactic-cni/galactic-tap-cni (and, when present, galactic-route) per
conflist order, never run standalone. Has zero kernel-interface
dependency: every address it advertises comes from prevResult, not from a
runtime call into an interface it doesn't own.

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

func newRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: appName,
Short: strings.Split(appDesc, "\n")[0],
Long: appDesc,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
cnibgp.InitCNIConfig()
confFile, _ := cmd.Flags().GetString("conf-file")
if confFile != "" {
cnibgp.ConfFile = confFile
}
return nil
},
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("%s version %s\n", appName, metadata.Version)
return nil
}
if os.Getenv("CNI_COMMAND") == "VERSION" {
return version.All.Encode(os.Stdout)
}

// Real CNI runtimes always pipe the network config JSON on
// stdin and close it. If stdin is an interactive terminal
// instead, no config will ever arrive and skel's blocking
// stdin read would hang forever — print version info instead.
if term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Printf("%s version %s\n", appName, metadata.Version)
fmt.Printf("CNI protocol versions supported: %s\n", strings.Join(version.All.SupportedVersions(), ", "))
return nil
}

// Unlike galactic-cni/galactic-tap-cni, this plugin never enters
// any network namespace at all (it only makes k8s API calls),
// so it needs neither the stdin peek-and-repipe dance nor
// CNI_NETNS_OVERRIDE those two use to detect and handle
// tap-mode's host-netns invocation.
cnibgp.RunPlugin()
return nil
},
}

cmd.PersistentFlags().String("conf-file", cnibgp.ConfFile, "Path to CNI conflist file")
cmd.Flags().Bool("build-info", false, "Print build information and exit")
cmd.Flags().BoolP("version", "V", false, "Print version and exit")
return cmd
}

func main() {
if err := newRootCommand().Execute(); err != nil {
log.Fatalf("error: %v", err)
}
}
15 changes: 15 additions & 0 deletions containers/galactic-cni/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build \
-X go.datum.net/galactic/internal/metadata.GitURL=${GIT_URL}" \
-o galactic-ipam cmd/galactic-ipam/main.go

# Build galactic-bgp, the BGP/SRv6/eBPF publish plugin in the galactic CNI
# chain. Ships in this same image/binary set for the same reason
# galactic-tap-cni/galactic-ipam do — see their own comments above.
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-bgp cmd/galactic-bgp/main.go

# Build vmtap-cni. It ships in this image rather than one of its own so the
# vmtap DaemonSet (config/vmtap/) can reference the same published
# ghcr.io/datum-cloud/galactic-cni image instead of a second, separately
Expand Down Expand Up @@ -111,6 +124,7 @@ FROM gcr.io/distroless/static:nonroot AS production
COPY --from=builder /workspace/galactic-cni /galactic-cni
COPY --from=builder /workspace/galactic-tap-cni /galactic-tap-cni
COPY --from=builder /workspace/galactic-ipam /galactic-ipam
COPY --from=builder /workspace/galactic-bgp /galactic-bgp
COPY --from=builder /workspace/vmtap-cni /vmtap-cni
COPY --from=builder /workspace/host-device /host-device
COPY --from=builder /var/run/galactic-cni /var/run/galactic-cni
Expand All @@ -125,6 +139,7 @@ FROM docker.io/library/alpine:latest
COPY --from=production /galactic-cni /galactic-cni
COPY --from=production /galactic-tap-cni /galactic-tap-cni
COPY --from=production /galactic-ipam /galactic-ipam
COPY --from=production /galactic-bgp /galactic-bgp
COPY --from=production /vmtap-cni /vmtap-cni
COPY --from=production /host-device /host-device
COPY --from=production /var/run/galactic-cni /var/run/galactic-cni
Expand Down
10 changes: 10 additions & 0 deletions docs/cni/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ v1.3.0 in `go.mod`) and returns CNI Result `1.0.0` (`type100`); generated
configs (the installer's default conflist, Multus `NetworkAttachmentDefinition`
manifests) use `"cniVersion": "1.0.0"`.

Despite that broad declared range, `cniVersion` must be `"1.0.0"` or `"1.1.0"`
in practice: `galactic-bgp`, chained after the master plugin, reconstructs
`prevResult` via `type100.NewResult` (`internal/cnibgp/prevresult.go`), which
only accepts a Result whose own `cniVersion` field is exactly one of those two
values — the master plugin echoes the conflist's `cniVersion` straight into
its printed Result, so an older value (e.g. `"0.4.0"`) makes `galactic-bgp`'s
ADD fail for every attachment in the chain. Every config in this doc already
uses `"1.0.0"`; keep it that way for any config authored outside these
examples.

### Interface Types

#### `veth` (default)
Expand Down
16 changes: 4 additions & 12 deletions internal/cni/cni_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package cni

import (
"context"
"errors"
"fmt"
"log/slog"
Expand Down Expand Up @@ -798,8 +797,7 @@ func TestResourceTrackerCleanupZeroValue(t *testing.T) {
// cleanup with a zero-value tracker must not panic — it's called in a
// defer and the caller may have failed before setting any fields.
tracker := &resourceTracker{}
ctx := context.Background()
tracker.cleanup(ctx) // should not panic
tracker.cleanup() // should not panic
}

func TestResourceTrackerCleanupPartialState(t *testing.T) {
Expand All @@ -808,17 +806,14 @@ func TestResourceTrackerCleanupPartialState(t *testing.T) {
tracker := &resourceTracker{
vpc: testVPC,
vpcAttachment: testAttachment,
namespace: "default",
}
ctx := context.Background()
tracker.cleanup(ctx) // should not panic; vrf.Delete will fail but is logged
tracker.cleanup() // should not panic; vrf.Delete will fail but is logged
}

func TestResourceTrackerFieldsSet(t *testing.T) {
tracker := &resourceTracker{
vpc: testVPC,
vpcAttachment: testAttachment,
namespace: "test-ns",
}

if tracker.vpc != testVPC {
Expand All @@ -827,14 +822,11 @@ func TestResourceTrackerFieldsSet(t *testing.T) {
if tracker.vpcAttachment != testAttachment {
t.Errorf("vpcAttachment = %q, want %q", tracker.vpcAttachment, testAttachment)
}
if tracker.namespace != "test-ns" {
t.Errorf("namespace = %q, want %q", tracker.namespace, "test-ns")
}
if tracker.vrfCreated {
t.Error("vrfCreated should be false by default")
}
if tracker.advCreated {
t.Error("advCreated should be false by default")
if tracker.routesCreated != 0 {
t.Error("routesCreated should be zero by default")
}
}

Expand Down
Loading