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
113 changes: 113 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod

- name: Run linter
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0

test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod

- name: Running Tests
run: |
go mod tidy
make test

# The matrix is DISCOVERED, so a new component is picked up by existing here rather than by
# someone remembering to add a line below. A component added without CI would never be built or
# tested by anything -- root `./...` stops at a nested go.mod. See components/README.md.
#
# Lint and test share this one job, which is why they live in one workflow: `needs` cannot reach a
# job in another file, so splitting them would mean two copies of the discovery rule -- and a rule
# changed in only one copy would lint a set of components and test a different set, silently.
discover:
name: Discover components
runs-on: ubuntu-latest
outputs:
components: ${{ steps.list.outputs.components }}
steps:
- name: Clone the code
uses: actions/checkout@v5

# Keyed on the Makefile, matching the root Makefile's own discovery.
- id: list
run: |
names=$(ls -d components/*/Makefile | xargs -n1 dirname | xargs -n1 basename | jq -R . | jq -sc .)
echo "components=$names" >> "$GITHUB_OUTPUT"

lint-components:
name: Lint components
runs-on: ubuntu-latest
needs: discover
strategy:
fail-fast: false
matrix:
component: ${{ fromJSON(needs.discover.outputs.components) }}
steps:
- name: Clone the code
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: components/${{ matrix.component }}/go.mod

- name: Run linter
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0
working-directory: components/${{ matrix.component }}
args: --config ${{ github.workspace }}/.golangci.yml

test-components:
name: Test components
runs-on: ubuntu-latest
needs: discover
strategy:
fail-fast: false
matrix:
component: ${{ fromJSON(needs.discover.outputs.components) }}
steps:
- name: Clone the code
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: components/${{ matrix.component }}/go.mod

- name: Running Tests
working-directory: components/${{ matrix.component }}
run: |
go mod tidy
git diff --exit-code go.mod go.sum
make test
23 changes: 0 additions & 23 deletions .github/workflows/lint.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod

Expand Down
23 changes: 0 additions & 23 deletions .github/workflows/test.yml

This file was deleted.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
bin/*
Dockerfile.cross

# A pattern containing a slash is anchored to this directory, so `bin/*` above does
# not reach a component's build output (see components/README.md).
components/*/bin/

# Locally built command binaries. `go build ./cmd/...` drops these in the repo
# ROOT, not bin/, so they are not covered above — and they are multi-MB, which is
# exactly the kind of thing that sneaks into a commit unnoticed.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.24 AS builder
FROM golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH
# VERSION is stamped into the binary (pkg/version) and surfaces as the virtual
Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
lint-config: golangci-lint ## Verify golangci-lint linter configuration
$(GOLANGCI_LINT) config verify

##@ Components

# Each component under components/ is its own Go module, so `./...` here cannot see any of it --
# not for build, test, vet or lint. This delegates any target to every component by name:
# `make components-lint`, `make components-test`. Adding a component needs no edit here.
#
# Without this, a nested module is simply never checked, which is the usual way one rots.
# Discovered by Makefile rather than by directory, which is both the contract (see
# components/README.md) and immune to make 3.81's wildcard not filtering on a trailing slash.
COMPONENTS := $(patsubst %/,%,$(dir $(wildcard components/*/Makefile)))

.PHONY: components-%
components-%: ## Run the named target in every component, e.g. make components-test.
@for c in $(COMPONENTS); do \
echo "==> $$c: $*"; \
$(MAKE) -C $$c $* || exit 1; \
done

##@ Build

.PHONY: build
Expand Down Expand Up @@ -238,7 +256,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.18.0
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
GOLANGCI_LINT_VERSION ?= v2.1.6
GOLANGCI_LINT_VERSION ?= v2.4.0

.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**The Control Plane for GPUaaS**

[![Discord](https://img.shields.io/badge/Discord-Join%20us-5865F2?logo=discord&logoColor=white)](https://discord.gg/7WTUuFqyS6)
![Go Version](https://img.shields.io/badge/go-1.24-00ADD8?logo=go&logoColor=white)
![Go Version](https://img.shields.io/badge/go-1.25-00ADD8?logo=go&logoColor=white)
[![Go Reference](https://pkg.go.dev/badge/github.com/InftyAI/Nebula.svg)](https://pkg.go.dev/github.com/InftyAI/Nebula)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

Expand Down
47 changes: 47 additions & 0 deletions components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Components

Optional pieces that ship separately from the manager. A cluster that deploys none of them is a
complete Nebula install.

Each subdirectory is its **own Go module**, deliberately: the root module cannot import a
component even by accident, so "the manager does not depend on this" is enforced by the module
boundary rather than by review. The dependency only runs the other way — a component may import
`github.com/InftyAI/Nebula` for the API types, via a `replace` on the repo root.

## The contract

Held to so that the root plumbing keeps working and the second component does not invent its own
conventions:

- **Its own `go.mod`**, with `replace github.com/InftyAI/Nebula => ../..` when it needs the API
types. The replace is what makes an API change break the component's build in the same PR
instead of drifting until someone bumps a tag. CI also runs `go mod tidy` and fails on a diff,
so an untidy `go.mod` is a red build rather than a surprise later.
- **Its own `Makefile` — which is what makes the component visible at all.** The root's
`components-%` rule and the CI matrix both discover components by globbing
`components/*/Makefile`, so a directory without one is checked by nothing, silently. `test` is
the target CI calls; `lint`, `build` and `docker-build` are called through the root by name
(`make components-lint`).
- **Its own `Dockerfile`, built with the repo root as context** — `docker build -f
components/<name>/Dockerfile .` — because the `replace` reaches outside the component
directory. A Dockerfile that assumes its own directory as context cannot resolve it.
- **Its own deployment, kept inside the component.** The root `config/` is the manager's install
and stays that way. A `config/` of its own or a script both work — logship uses
[`logship/hack/deploy.sh`](logship/hack/deploy.sh).
- **`internal/` for everything but `cmd/`**, because nothing outside the component should import
it.

## Why the root tooling does not reach inside

`go build ./...` skips directories containing their own `go.mod`, so the root's `lint`, `test`
and `vet` targets do not see any of this — silently, which is the usual way a nested module rots.
Two things prevent it: the `components-%` pattern rule in the root `Makefile`, and the `discover`
job in `.github/workflows/ci.yml` that fans lint and test out over one job per component. Both
build the list from `components/*/Makefile`, so adding a component needs no edit to either.

## Components

- [`logship/`](logship/) — copies the logs of the instances Nebula runs outside the cluster to
CloudWatch Logs, so a run can still be explained after the provider's own retention window
closes. Modal is the first backend, behind a port. [How to run it](logship/README.md);
[why it is shaped this way](logship/design.md).
42 changes: 42 additions & 0 deletions components/logship/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Build the logship binary.
#
# THE BUILD CONTEXT IS THE REPO ROOT, not this directory:
#
# docker build -f components/logship/Dockerfile .
#
# go.mod carries `replace github.com/InftyAI/Nebula => ../..` for the API types, so a context
# rooted at this directory cannot resolve the replacement and `go mod download` fails.
#
# --platform=${BUILDPLATFORM} pins the builder stage to the machine doing the building and leaves the
# target to GOOS/GOARCH below. Without it, buildx runs an arm64 builder under emulation to produce the
# arm64 image — minutes of QEMU per architecture for a cross-compile Go does for free. It is also why
# this needs no Dockerfile.cross sed dance like the root's docker-buildx.
FROM --platform=${BUILDPLATFORM} golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace

# The replaced module's go.mod must be present for the component's own resolution to work, even
# where no package from it is imported yet.
COPY go.mod go.mod
COPY go.sum go.sum
COPY api/ api/

WORKDIR /workspace/components/logship
COPY components/logship/go.mod go.mod
COPY components/logship/go.sum go.sum
RUN go mod download

COPY components/logship/cmd/ cmd/
COPY components/logship/internal/ internal/

RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -a -o logship ./cmd

FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/components/logship/logship .
USER 65532:65532

ENTRYPOINT ["/logship"]
Loading
Loading