Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3bda573
Update golangci lint
mangelajo Oct 21, 2025
8c13c62
operator: add RBAC permissions for the operator
mangelajo Oct 13, 2025
3d61611
operator: basic controller and endpoint reconciler
mangelajo Oct 13, 2025
8c83b11
operator: build and deploy of operator from main Makefile
mangelajo Oct 15, 2025
e42c890
operator: make jumpstarter-controller single-namespace
mangelajo Oct 17, 2025
80639b5
operator: start deploying services, RBAC and CRDs
mangelajo Oct 17, 2025
07d8c50
operator: include and print build details
mangelajo Oct 17, 2025
302bc90
operator: minimal config for now in the deploy operator
mangelajo Oct 17, 2025
4843439
operator: compatible secrets
mangelajo Oct 17, 2025
4014ce7
operator: add caching to container builds for local devel
mangelajo Oct 21, 2025
58fec83
operator: deploy router replicas and endpoints
mangelajo Oct 21, 2025
f65267c
operator: support ClusterIP service types and create all types
mangelajo Oct 22, 2025
32ff96f
operator: use address for endpoints instead of hostname
mangelajo Oct 22, 2025
8b2d973
operator: config map from go structures vs manual yaml
mangelajo Oct 22, 2025
94520d4
operator: move build docker context to toplevel
mangelajo Oct 22, 2025
824d743
operator: move endpoint handling to endpoint reconciler
mangelajo Oct 22, 2025
6e84764
operator: add basic job to deploy with operator
mangelajo Oct 22, 2025
6aee77a
operator: preserve immutable service fields
mangelajo Oct 23, 2025
554c16c
operator: simplify endpoint controller, and remove duplication
mangelajo Oct 23, 2025
a77b4c1
operator: remove envtest duplication
mangelajo Oct 23, 2025
e5b7e5d
operator: safer IMAGE_TAG/REPO
mangelajo Oct 23, 2025
77df0f4
operator: fix endless reconciliation loops
mangelajo Oct 23, 2025
096b27a
operator: do not own the service account to avoid garbage collection
mangelajo Oct 24, 2025
9392eb9
operator: include version information in the router too
mangelajo Oct 24, 2025
f1668b0
operator: set controller references correctly
mangelajo Oct 27, 2025
4d5c98d
operator: complete rbac for accesspolicies
mangelajo Oct 27, 2025
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
14 changes: 14 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ jobs:
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "VERSION=${VERSION}"

- name: Set build args
id: build-args
run: |
GIT_COMMIT=$(git rev-parse HEAD)
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo "git_commit=${GIT_COMMIT}" >> $GITHUB_OUTPUT
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "GIT_COMMIT=${GIT_COMMIT}"
echo "BUILD_DATE=${BUILD_DATE}"

- name: Set image tags
id: set-tags
run: |
Expand Down Expand Up @@ -79,6 +89,10 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
GIT_VERSION=${{ env.VERSION }}
GIT_COMMIT=${{ steps.build-args.outputs.git_commit }}
BUILD_DATE=${{ steps.build-args.outputs.build_date }}

publish-helm-charts-containers:
needs: build-and-push-image
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
controller-ref: ${{ github.ref }}
# use the matching branch on the jumpstarter repo
jumpstarter-ref: ${{ github.event.pull_request.base.ref }}
e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e:
e2e-tests-release-0-7:
runs-on: ubuntu-latest
steps:
- uses: jumpstarter-dev/jumpstarter-e2e@11a5ce6734be9f089ec3ea6ebf55284616f67fe8
- uses: jumpstarter-dev/jumpstarter-e2e@release-0.7
with:
controller-ref: ${{ github.ref }}
jumpstarter-ref: 28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
jumpstarter-ref: release-0.7
11 changes: 11 additions & 0 deletions .github/workflows/pr-kind.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ jobs:

- name: Run make deploy
run: make deploy

deploy-with-operator:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run make deploy
run: make deploy-with-operator
22 changes: 19 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
FROM registry.access.redhat.com/ubi9/go-toolset:1.24.6 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG GIT_VERSION=unknown
ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown

# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Cache module downloads across builds
RUN --mount=type=cache,target=/opt/app-root/src/go/pkg/mod,sharing=locked,uid=1001,gid=0 \
--mount=type=cache,target=/opt/app-root/src/.cache/go-build,sharing=locked,uid=1001,gid=0 \
go mod download

# Copy the go source
COPY cmd/ cmd/
Expand All @@ -20,8 +26,18 @@ COPY internal/ internal/
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o router cmd/router/main.go
RUN --mount=type=cache,target=/opt/app-root/src/go/pkg/mod,sharing=locked,uid=1001,gid=0 \
--mount=type=cache,target=/opt/app-root/src/.cache/go-build,sharing=locked,uid=1001,gid=0 \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -a \
-ldflags "-X main.version=${GIT_VERSION} -X main.gitCommit=${GIT_COMMIT} -X main.buildDate=${BUILD_DATE}" \
-o manager cmd/main.go
RUN --mount=type=cache,target=/opt/app-root/src/go/pkg/mod,sharing=locked,uid=1001,gid=0 \
--mount=type=cache,target=/opt/app-root/src/.cache/go-build,sharing=locked,uid=1001,gid=0 \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -a \
-ldflags "-X main.version=${GIT_VERSION} -X main.gitCommit=${GIT_COMMIT} -X main.buildDate=${BUILD_DATE}" \
-o router cmd/router/main.go

FROM registry.access.redhat.com/ubi9/ubi-micro:9.5
WORKDIR /
Expand Down
47 changes: 47 additions & 0 deletions Dockerfile.operator
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Build the manager binary
FROM registry.access.redhat.com/ubi9/go-toolset:1.24.6 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG GIT_VERSION=unknown
ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown

# Copy the Go Modules manifests
COPY --chown=1001:0 deploy/operator/go.mod deploy/operator/go.mod
COPY --chown=1001:0 deploy/operator/go.sum deploy/operator/go.sum
COPY --chown=1001:0 go.mod go.mod
COPY --chown=1001:0 go.sum go.sum


# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN --mount=type=cache,target=/opt/app-root/src/go/pkg/mod,sharing=locked,uid=1001,gid=0 \
--mount=type=cache,target=/opt/app-root/src/.cache/go-build,sharing=locked,uid=1001,gid=0 \
cd deploy/operator && go mod download

# Copy the base jumpstarter-controller internal/config parts
COPY --chown=1001:0 internal/ internal/
COPY --chown=1001:0 api/ api/
# Copy the go source
COPY --chown=1001:0 deploy/operator/cmd/ deploy/operator/cmd/
COPY --chown=1001:0 deploy/operator/api/ deploy/operator/api/
COPY --chown=1001:0 deploy/operator/internal/ deploy/operator/internal/

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN --mount=type=cache,target=/opt/app-root/src/go/pkg/mod,sharing=locked,uid=1001,gid=0 \
--mount=type=cache,target=/opt/app-root/src/.cache/go-build,sharing=locked,uid=1001,gid=0 \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
cd deploy/operator && go build -a \
-ldflags "-X main.version=${GIT_VERSION} -X main.gitCommit=${GIT_COMMIT} -X main.buildDate=${BUILD_DATE}" \
-o manager cmd/main.go

FROM registry.access.redhat.com/ubi9/ubi-micro:9.5
WORKDIR /
COPY --from=builder /opt/app-root/src/deploy/operator/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
41 changes: 37 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ DOCKER_TAG = $(shell echo $(IMG) | cut -d: -f2)
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.30.0

# Version information
GIT_VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "unknown")
GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')

# LDFLAGS for version information
LDFLAGS := -X main.version=$(GIT_VERSION) \
-X main.gitCommit=$(GIT_COMMIT) \
-X main.buildDate=$(BUILD_DATE)

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
Expand Down Expand Up @@ -53,6 +63,8 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust
output:crd:artifacts:config=deploy/helm/jumpstarter/crds/ \
output:rbac:artifacts:config=deploy/helm/jumpstarter/charts/jumpstarter-controller/templates/rbac/

cp deploy/helm/jumpstarter/crds/* deploy/operator/config/crd/bases/

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..." paths="./internal/..."
Expand Down Expand Up @@ -83,11 +95,14 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
$(GOLANGCI_LINT) run --fix

##@ Build
.PHONY: build-operator
build-operator:
make -C deploy/operator build-installer docker-build

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
go build -o bin/manager cmd/main.go
go build -o bin/router cmd/router/main.go
go build -ldflags "$(LDFLAGS)" -o bin/manager cmd/main.go
go build -ldflags "$(LDFLAGS)" -o bin/router cmd/router/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
Expand All @@ -102,7 +117,11 @@ run-router: manifests generate fmt vet ## Run a router from your host.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
$(CONTAINER_TOOL) build -t ${IMG} .
$(CONTAINER_TOOL) build \
--build-arg GIT_VERSION=$(GIT_VERSION) \
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
--build-arg BUILD_DATE=$(BUILD_DATE) \
-t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
Expand All @@ -122,6 +141,9 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform
- $(CONTAINER_TOOL) buildx create --name jumpstarter-controller-builder
$(CONTAINER_TOOL) buildx use jumpstarter-controller-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) \
--build-arg GIT_VERSION=$(GIT_VERSION) \
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
--build-arg BUILD_DATE=$(BUILD_DATE) \
--tag ${DOCKER_REPO}:${DOCKER_TAG} \
--tag ${DOCKER_REPO}:latest \
-f Dockerfile.cross .
Expand Down Expand Up @@ -152,6 +174,17 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
deploy: docker-build cluster grpcurl
./hack/deploy_with_helm.sh

.PHONY: deploy-with-operator
deploy-with-operator: docker-build build-operator cluster grpcurl
./hack/deploy_with_operator.sh

.PHONY: operator-logs
operator-logs:
kubectl logs -n jumpstarter-operator-system -l app.kubernetes.io/name=jumpstarter-operator -f

deploy-with-operator-parallel:
make deploy-with-operator -j5 --output-sync=target

.PHONY: deploy-exporters
deploy-exporters:
./hack/demoenv/prepare_exporters.sh
Expand Down Expand Up @@ -185,7 +218,7 @@ GRPCURL = $(LOCALBIN)/grpcurl
KUSTOMIZE_VERSION ?= v5.4.1
CONTROLLER_TOOLS_VERSION ?= v0.16.3
ENVTEST_VERSION ?= release-0.18
GOLANGCI_LINT_VERSION ?= v2.1.2
GOLANGCI_LINT_VERSION ?= v2.5.0
KIND_VERSION ?= v0.27.0
GRPCURL_VERSION ?= v1.9.2

Expand Down
65 changes: 63 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
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/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
Expand All @@ -55,8 +56,42 @@ import (
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")

// Version information - set via ldflags at build time
version = "dev"
gitCommit = "unknown"
buildDate = "unknown"
)

const (
// namespaceFile is the path to the namespace file mounted by Kubernetes
namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
)

// getWatchNamespace returns the namespace the controller should watch.
// It tries multiple sources in order:
// 1. NAMESPACE environment variable (explicit configuration takes precedence)
// 2. Namespace file (automatically mounted by Kubernetes in every pod)
// 3. Empty string (will fail, not supported since 0.8.0)
func getWatchNamespace() string {
// First check NAMESPACE environment variable (explicit configuration)
if ns := os.Getenv("NAMESPACE"); ns != "" {
setupLog.Info("Using namespace from NAMESPACE environment variable", "namespace", ns)
return ns
}

// Fall back to reading from the namespace file mounted by Kubernetes
if ns, err := os.ReadFile(namespaceFile); err == nil {
namespace := string(ns)
if namespace != "" {
setupLog.Info("Auto-detected namespace from service account", "namespace", namespace)
return namespace
}
}

return ""
}

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))

Expand Down Expand Up @@ -90,6 +125,13 @@ func main() {

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// Print version information
setupLog.Info("Jumpstarter Controller starting",
"version", version,
"gitCommit", gitCommit,
"buildDate", buildDate,
)

// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
Expand All @@ -110,7 +152,11 @@ func main() {
TLSOpts: tlsOpts,
})

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
// Get the namespace to watch. Try to auto-detect from the pod's service account,
// fall back to NAMESPACE environment variable, or watch all namespaces if neither is available
watchNamespace := getWatchNamespace()

mgrOptions := ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
Expand All @@ -132,7 +178,22 @@ func main() {
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
}

// If a specific namespace is set, configure the cache to only watch that namespace
if watchNamespace != "" {
mgrOptions.LeaderElectionNamespace = watchNamespace
mgrOptions.Cache = cache.Options{
DefaultNamespaces: map[string]cache.Config{
watchNamespace: {},
},
}
} else {
setupLog.Error(nil, "Jumpstarter controller can only be configured to work on a single namespace since 0.8.0")
os.Exit(1)
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
Expand Down
14 changes: 14 additions & 0 deletions cmd/router/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ import (
_ "google.golang.org/grpc/encoding/gzip"
)

var (
// Version information - set via ldflags at build time
version = "dev"
gitCommit = "unknown"
buildDate = "unknown"
)

func main() {
opts := zap.Options{}
opts.BindFlags(flag.CommandLine)
Expand All @@ -44,6 +51,13 @@ func main() {
logger := ctrl.Log.WithName("router")
ctx := logr.NewContext(context.Background(), logger)

// Print version information
logger.Info("Jumpstarter Router starting",
"version", version,
"gitCommit", gitCommit,
"buildDate", buildDate,
)

cfg := ctrl.GetConfigOrDie()
client, err := kclient.New(cfg, kclient.Options{})
if err != nil {
Expand Down
Loading
Loading