proxy-operator: Kubernetes operator for crawling-proxy fleets #1
@@ -2,6 +2,7 @@
|
|||||||
FROM golang:1.26 AS builder
|
FROM golang:1.26 AS builder
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
# Copy the Go Modules manifests
|
# 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
|
# 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,
|
# 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.
|
# 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
|
# Use distroless as minimal base image to package the manager binary
|
||||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||||
FROM gcr.io/distroless/static:nonroot
|
FROM gcr.io/distroless/static:nonroot
|
||||||
|
ARG GIT_COMMIT=unknown
|
||||||
|
LABEL org.opencontainers.image.revision="${GIT_COMMIT}"
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=builder /workspace/manager .
|
COPY --from=builder /workspace/manager .
|
||||||
USER 65532:65532
|
USER 65532:65532
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -2,6 +2,9 @@
|
|||||||
IMG ?= controller:latest
|
IMG ?= controller:latest
|
||||||
# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
|
# YEAR defines the year value used for substituting the YEAR placeholder in the boilerplate header.
|
||||||
YEAR ?= $(shell date +%Y)
|
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)
|
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||||
ifeq (,$(shell go env GOBIN))
|
ifeq (,$(shell go env GOBIN))
|
||||||
@@ -110,7 +113,7 @@ lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
|||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: manifests generate fmt vet ## Build manager binary.
|
build: manifests generate fmt vet ## Build manager binary.
|
||||||
go build -o bin/manager cmd/main.go
|
go build -o bin/manager ./cmd
|
||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: manifests generate fmt vet ## Run a controller from your host.
|
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/
|
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build: ## Build docker image with the manager.
|
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
|
.PHONY: docker-push
|
||||||
docker-push: ## Push docker image with the manager.
|
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
|
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 create --name egress-proxies-operator-builder
|
||||||
$(CONTAINER_TOOL) buildx use 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
|
- $(CONTAINER_TOOL) buildx rm egress-proxies-operator-builder
|
||||||
rm Dockerfile.cross
|
rm Dockerfile.cross
|
||||||
|
|
||||||
|
|||||||
13
cmd/main.go
13
cmd/main.go
@@ -24,7 +24,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
goruntime "runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
// 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/gcp"
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/kubernetes"
|
"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/provider/registry"
|
||||||
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/version"
|
||||||
// +kubebuilder:scaffold:imports
|
// +kubebuilder:scaffold:imports
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,6 +91,7 @@ func main() {
|
|||||||
var gcInterval, gcMinAge time.Duration
|
var gcInterval, gcMinAge time.Duration
|
||||||
var gcAllowNamespaced bool
|
var gcAllowNamespaced bool
|
||||||
var leaseCooldown, maxLeaseTTL time.Duration
|
var leaseCooldown, maxLeaseTTL time.Duration
|
||||||
|
var showVersion bool
|
||||||
|
|
||||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
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.")
|
"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.")
|
"How long a reported proxy/target pair is excluded from lease selection.")
|
||||||
flag.DurationVar(&maxLeaseTTL, "max-lease-ttl", time.Hour,
|
flag.DurationVar(&maxLeaseTTL, "max-lease-ttl", time.Hour,
|
||||||
"Maximum lease TTL a client may request.")
|
"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{
|
opts := zap.Options{
|
||||||
Development: true,
|
Development: true,
|
||||||
@@ -131,7 +137,14 @@ func main() {
|
|||||||
opts.BindFlags(flag.CommandLine)
|
opts.BindFlags(flag.CommandLine)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if showVersion {
|
||||||
|
fmt.Println(version.Resolve())
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||||
|
setupLog.Info("Starting egress-proxies-operator",
|
||||||
|
"commit", version.Resolve(), "goVersion", goruntime.Version())
|
||||||
ctx := ctrl.SetupSignalHandler()
|
ctx := ctrl.SetupSignalHandler()
|
||||||
|
|
||||||
// Providers load first and fail fast: a manager that comes up without
|
// 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