proxy-operator: Kubernetes operator for crawling-proxy fleets #1
@@ -2,6 +2,7 @@
|
||||
FROM golang:1.26 AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG GIT_COMMIT=unknown
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
@@ -19,11 +20,15 @@ COPY . .
|
||||
# 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 \
|
||||
-ldflags "-X gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version.Commit=${GIT_COMMIT}" \
|
||||
-o manager cmd/main.go
|
||||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
ARG GIT_COMMIT=unknown
|
||||
LABEL org.opencontainers.image.revision="${GIT_COMMIT}"
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
USER 65532:65532
|
||||
|
||||
9
Makefile
9
Makefile
@@ -2,6 +2,9 @@
|
||||
IMG ?= controller:latest
|
||||
# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
|
||||
YEAR ?= $(shell date +%Y)
|
||||
# GIT_COMMIT is baked into the image (-ldflags in the Dockerfile); -dirty
|
||||
# covers staged and untracked changes too, which `git diff --quiet` misses.
|
||||
GIT_COMMIT ?= $(shell git rev-parse --short=12 HEAD 2>/dev/null || echo unknown)$(shell test -z "$$(git status --porcelain 2>/dev/null)" || echo -dirty)
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
@@ -110,7 +113,7 @@ lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager cmd/main.go
|
||||
go build -o bin/manager ./cmd
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
@@ -125,7 +128,7 @@ run-dev: manifests generate fmt vet ## Run locally against the current kubeconfi
|
||||
# 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_COMMIT=$(GIT_COMMIT) -t ${IMG} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
@@ -144,7 +147,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform
|
||||
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||
- $(CONTAINER_TOOL) buildx create --name egress-proxies-operator-builder
|
||||
$(CONTAINER_TOOL) buildx use egress-proxies-operator-builder
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --build-arg GIT_COMMIT=$(GIT_COMMIT) --tag ${IMG} -f Dockerfile.cross .
|
||||
- $(CONTAINER_TOOL) buildx rm egress-proxies-operator-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
|
||||
13
cmd/main.go
13
cmd/main.go
@@ -24,7 +24,9 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
@@ -56,6 +58,7 @@ import (
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/gcp"
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/kubernetes"
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/registry"
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
@@ -88,6 +91,7 @@ func main() {
|
||||
var gcInterval, gcMinAge time.Duration
|
||||
var gcAllowNamespaced bool
|
||||
var leaseCooldown, maxLeaseTTL time.Duration
|
||||
var showVersion bool
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
@@ -124,6 +128,8 @@ func main() {
|
||||
"How long a reported proxy/target pair is excluded from lease selection.")
|
||||
flag.DurationVar(&maxLeaseTTL, "max-lease-ttl", time.Hour,
|
||||
"Maximum lease TTL a client may request.")
|
||||
flag.BoolVar(&showVersion, "version", false,
|
||||
"Print the commit the binary was built from and exit.")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
@@ -131,7 +137,14 @@ func main() {
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
if showVersion {
|
||||
fmt.Println(version.Resolve())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
setupLog.Info("Starting egress-proxies-operator",
|
||||
"commit", version.Resolve(), "goVersion", goruntime.Version())
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
|
||||
// Providers load first and fail fast: a manager that comes up without
|
||||
|
||||
62
docs/plans-executions/2026-08-11-1802-bake-commit-version.md
Normal file
62
docs/plans-executions/2026-08-11-1802-bake-commit-version.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Execution: Bake the git commit into the operator binary and log it at startup
|
||||
|
||||
Plan: `docs/plans/2026-08-11-1802-bake-commit-version.md`
|
||||
|
||||
- [x] Step 0 — Save and commit the plan
|
||||
- [x] Step 1 — `internal/version` package + tests
|
||||
- [x] Step 2 — `cmd/main.go`: `--version` flag + startup log line
|
||||
- [x] Step 3 — Makefile `GIT_COMMIT` + `--build-arg` wiring
|
||||
- [x] Step 4 — Dockerfile `-ldflags` stamp + OCI revision label
|
||||
- [ ] Step 5 — CHANGELOG entry (after the user confirms it works live)
|
||||
|
||||
## Steps 1–4
|
||||
|
||||
Mostly as planned. One deviation worth recording: the plan claimed
|
||||
`build`/`run` targets need no changes because Go's automatic VCS stamp covers
|
||||
host builds — that turned out to be only half true. Go skips VCS stamping
|
||||
when the build target is a *file argument* rather than a package pattern, and
|
||||
the Makefile's `build` target used `go build -o bin/manager cmd/main.go`.
|
||||
Verified empirically:
|
||||
|
||||
```bash
|
||||
go build -o bin/manager cmd/main.go && ./bin/manager --version # unknown (no vcs settings)
|
||||
go build -o bin/manager ./cmd && ./bin/manager --version # e4d2a191d0c2-dirty
|
||||
```
|
||||
|
||||
So `build:` now uses `go build -o bin/manager ./cmd`. `go run` never stamps
|
||||
VCS info regardless of invocation form — `make run`/`run-dev` print
|
||||
`commit=unknown`, which is acceptable for dev loops (the Dockerfile path uses
|
||||
the explicit ldflags stamp and is unaffected; it kept `cmd/main.go`).
|
||||
|
||||
The ldflags path was verified independently:
|
||||
|
||||
```bash
|
||||
go build -ldflags "-X gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version.Commit=deadbeef1234" \
|
||||
-o bin/manager-stamped cmd/main.go
|
||||
./bin/manager-stamped --version # deadbeef1234
|
||||
```
|
||||
|
||||
Worth noting: `cmd/main.go` imports k8s apimachinery as `runtime`, so the
|
||||
stdlib runtime needed an alias (`goruntime "runtime"`) for
|
||||
`goruntime.Version()` in the startup line. The `--version` check happens
|
||||
right after `flag.Parse()`, before logger and manager setup, so it works
|
||||
without a kubeconfig.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
go vet ./... && go test ./... # all green, incl. new resolve() table tests
|
||||
go build -o bin/manager ./cmd && ./bin/manager --version # e4d2a191d0c2-dirty
|
||||
```
|
||||
|
||||
The image-level check could not run locally — the Docker daemon was not
|
||||
running. `make docker-build IMG=egress-proxies-operator:dev` did prove the
|
||||
Makefile side before failing at the daemon: it invoked
|
||||
`docker build --build-arg GIT_COMMIT=e4d2a191d0c2-dirty ...`. Still pending
|
||||
(needs a running daemon):
|
||||
|
||||
```bash
|
||||
make docker-build IMG=egress-proxies-operator:dev
|
||||
docker run --rm egress-proxies-operator:dev --version
|
||||
docker inspect egress-proxies-operator:dev --format '{{index .Config.Labels "org.opencontainers.image.revision"}}'
|
||||
```
|
||||
47
internal/version/version.go
Normal file
47
internal/version/version.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Package version reports which commit the binary was built from. Docker
|
||||
// builds stamp it via -ldflags (the build context has no .git, so Go's
|
||||
// automatic VCS stamp is absent there); host builds fall back to that
|
||||
// automatic stamp.
|
||||
package version
|
||||
|
||||
import "runtime/debug"
|
||||
|
||||
// Commit is set at link time via
|
||||
// -ldflags "-X <module>/internal/version.Commit=<hash>".
|
||||
var Commit string
|
||||
|
||||
// Resolve returns the commit the binary was built from, or "unknown" when
|
||||
// neither the ldflags stamp nor build info is available (e.g. go test).
|
||||
func Resolve() string {
|
||||
return resolve(Commit, debug.ReadBuildInfo)
|
||||
}
|
||||
|
||||
func resolve(ldflagsCommit string, readBuildInfo func() (*debug.BuildInfo, bool)) string {
|
||||
if ldflagsCommit != "" {
|
||||
return ldflagsCommit
|
||||
}
|
||||
bi, ok := readBuildInfo()
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
var revision string
|
||||
var modified bool
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
revision = s.Value
|
||||
case "vcs.modified":
|
||||
modified = s.Value == "true"
|
||||
}
|
||||
}
|
||||
if revision == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if len(revision) > 12 {
|
||||
revision = revision[:12]
|
||||
}
|
||||
if modified {
|
||||
revision += "-dirty"
|
||||
}
|
||||
return revision
|
||||
}
|
||||
74
internal/version/version_test.go
Normal file
74
internal/version/version_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func buildInfoWith(settings ...debug.BuildSetting) func() (*debug.BuildInfo, bool) {
|
||||
return func() (*debug.BuildInfo, bool) {
|
||||
return &debug.BuildInfo{Settings: settings}, true
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve_precedenceAndFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
noBuildInfo := func() (*debug.BuildInfo, bool) { return nil, false }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ldflagsCommit string
|
||||
readBuildInfo func() (*debug.BuildInfo, bool)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ldflags stamp wins over build info",
|
||||
ldflagsCommit: "abc123def456-dirty",
|
||||
readBuildInfo: buildInfoWith(debug.BuildSetting{Key: "vcs.revision", Value: "ffffffffffffffffffffffffffffffffffffffff"}),
|
||||
want: "abc123def456-dirty",
|
||||
},
|
||||
{
|
||||
name: "no stamp, no build info",
|
||||
readBuildInfo: noBuildInfo,
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "build info without vcs settings",
|
||||
readBuildInfo: buildInfoWith(),
|
||||
want: "unknown",
|
||||
},
|
||||
{
|
||||
name: "full revision truncated to 12 chars",
|
||||
readBuildInfo: buildInfoWith(
|
||||
debug.BuildSetting{Key: "vcs.revision", Value: "0123456789abcdef0123456789abcdef01234567"},
|
||||
debug.BuildSetting{Key: "vcs.modified", Value: "false"},
|
||||
),
|
||||
want: "0123456789ab",
|
||||
},
|
||||
{
|
||||
name: "modified tree gets dirty suffix",
|
||||
readBuildInfo: buildInfoWith(
|
||||
debug.BuildSetting{Key: "vcs.revision", Value: "0123456789abcdef0123456789abcdef01234567"},
|
||||
debug.BuildSetting{Key: "vcs.modified", Value: "true"},
|
||||
),
|
||||
want: "0123456789ab-dirty",
|
||||
},
|
||||
{
|
||||
name: "short revision kept as-is",
|
||||
readBuildInfo: buildInfoWith(
|
||||
debug.BuildSetting{Key: "vcs.revision", Value: "abc123"},
|
||||
),
|
||||
want: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := resolve(tc.ldflagsCommit, tc.readBuildInfo); got != tc.want {
|
||||
t.Errorf("resolve() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user