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
52 changes: 52 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Configuration for GitHub's automatic release notes.
#
# Used by `gh release create --generate-notes` and by the "Generate release
# notes" button in the UI. Categories are matched in order, so the first match
# wins — which is why `breaking` sits at the top.
#
# Kept identical across the five kube-workspaces repositories so a reader moving
# between them sees the same structure. If you change it here, change it
# everywhere (scripts/check-release-config.sh in the deploy repo enforces this).
changelog:
exclude:
labels:
- duplicate
- invalid
- wontfix
- question

categories:
- title: ⚠️ Breaking changes
labels:
- breaking

- title: 🔒 Security
labels:
- security

- title: ✨ Features
labels:
- enhancement
- feature

- title: 🐛 Fixes
labels:
- bug

- title: 📚 Documentation
labels:
- documentation

- title: 🔧 CI and tooling
labels:
- ci
- chore

- title: ⬆️ Dependencies
labels:
- dependencies

# Anything unlabelled still appears, rather than being silently dropped.
- title: Other changes
labels:
- "*"
5 changes: 5 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ jobs:
# runners. Buildx was already set up; the platforms list was simply
# never passed.
platforms: linux/amd64,linux/arm64
# Compiled into the binary, so a running pod can report what it is.
build-args: |
VERSION=${{ steps.meta.outputs.version }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
106 changes: 106 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Release

# Cut a release: tag the commit, generate notes, publish.
#
# Run from the Actions tab with the version to release. Tagging triggers the
# Docker workflow, which publishes the versioned image.
#
# Release the four component repositories BEFORE kube-workspaces/deploy: the
# chart's appVersion pins these images and must name ones that already exist.
# See https://github.com/kube-workspaces/deploy/blob/main/docs/releasing.md

on:
workflow_dispatch:
inputs:
version:
description: 'Version to release, e.g. v0.3.0'
type: string
required: true
dry_run:
description: 'Print the notes without tagging or publishing'
type: boolean
default: true

permissions:
contents: write

jobs:
release:
name: Release ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
# Full history: the notes are generated from the commit range since the
# previous tag.
fetch-depth: 0

- name: Validate the version
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::'$VERSION' is not a vX.Y.Z version"
exit 1
fi
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "::error::tag $VERSION already exists"
exit 1
fi

# The generator lives in the deploy repo so all five repositories classify
# commits the same way.
- name: Fetch the release-notes generator
run: |
curl -fsSLo /tmp/release-notes.sh \
https://raw.githubusercontent.com/kube-workspaces/deploy/main/scripts/release-notes.sh
chmod +x /tmp/release-notes.sh

- name: Generate release notes
id: notes
run: |
set -euo pipefail
/tmp/release-notes.sh \
--repo "${GITHUB_REPOSITORY##*/}" \
--from "$(git describe --tags --abbrev=0)" \
--to HEAD > /tmp/preamble.md
cat /tmp/preamble.md
{
echo 'body<<RELEASE_NOTES_EOF'
cat /tmp/preamble.md
echo 'RELEASE_NOTES_EOF'
} >> "$GITHUB_OUTPUT"

- name: Summary
run: |
{
echo "### Release notes preview for ${{ inputs.version }}"
echo
cat /tmp/preamble.md
} >> "$GITHUB_STEP_SUMMARY"

- name: Create the tag and release
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git tag -a "$VERSION" -m "$VERSION"
git push origin "$VERSION"

# --generate-notes appends the categorised commit list, grouped per
# .github/release.yml, beneath our preamble.
gh release create "$VERSION" \
--title "$VERSION" \
--notes "${{ steps.notes.outputs.body }}" \
--generate-notes \
--verify-tag

- name: Dry run notice
if: ${{ inputs.dry_run }}
run: |
echo "::notice::dry run — nothing was tagged or published. Re-run with dry_run=false to release."
11 changes: 10 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
ARG TARGETOS
ARG TARGETARCH

# Build information, surfaced by --version, the startup log and /version.
ARG VERSION=dev
ARG COMMIT=unknown
ARG BUILD_DATE=unknown

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

COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -a -o kube-workspaces-api ./cmd/kube_workspaces/
go build -a -ldflags "\
-X main.version=${VERSION} \
-X main.commit=${COMMIT} \
-X main.buildDate=${BUILD_DATE}" \
-o kube-workspaces-api ./cmd/kube_workspaces/

FROM gcr.io/distroless/static:nonroot
LABEL org.opencontainers.image.source="https://github.com/kube-workspaces/api"
Expand Down
47 changes: 46 additions & 1 deletion cmd/kube_workspaces/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/fs"
"net/http"
"net/url"
"runtime"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -162,6 +163,48 @@ func handleHTTPServer(ctx context.Context, u *url.URL, workspacesEndpoints *work
mux.Handle("POST", "/auth/logout", oidcHandler.HandleLogout)
mux.Handle("GET", "/auth/me", oidcHandler.HandleMe)

// Platform version endpoint (public). Reports this build plus the image each
// component is actually running, so "what is deployed here?" can be answered
// without cluster access. The API's own version is compiled in; the others are
// read from their pod specs, since a component cannot be asked over the
// network without assuming it is healthy.
mux.Handle("GET", "/platform/version", func(w http.ResponseWriter, r *http.Request) {
v, c, d := Version()
resp := map[string]interface{}{
"api": map[string]string{
"version": v,
"commit": c,
"buildDate": d,
"go": runtime.Version(),
"platform": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
},
}

ns := os.Getenv("POD_NAMESPACE")
if ns == "" {
ns = "kube-workspaces-system"
}
components := map[string]string{}
for _, comp := range []string{"controller", "api", "proxy", "frontend"} {
pods, err := coreClient.ListPods(r.Context(), ns,
"app.kubernetes.io/component="+comp)
if err != nil || pods == nil || len(pods.Items) == 0 {
continue
}
pod := pods.Items[0]
if len(pod.Spec.Containers) > 0 {
components[comp] = pod.Spec.Containers[0].Image
}
}
if len(components) > 0 {
resp["images"] = components
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
})

// Platform config endpoint (public, returns form locks + maintenance status)
mux.Handle("GET", "/platform/config", func(w http.ResponseWriter, r *http.Request) {
cfg, err := platformProvider.GetConfig(r.Context())
Expand Down Expand Up @@ -1609,7 +1652,9 @@ func maintenanceMiddleware(pp *platform.ConfigProvider, next http.Handler) http.
exemptPrefixes := []string{
"/health",
"/auth/",
"/platform/config",
// Covers both /platform/config and /platform/version: neither should be
// blocked by maintenance mode, since both are used to diagnose it.
"/platform/",
"/admin/",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
38 changes: 38 additions & 0 deletions cmd/kube_workspaces/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"
"os"
"os/signal"
"runtime"
"sync"
"syscall"
"time"
Expand All @@ -24,6 +25,28 @@ import (
"goa.design/clue/log"
)

// Build information, injected at link time:
//
// go build -ldflags "-X main.version=v1.2.3 -X main.commit=abc1234 -X main.buildDate=..."
//
// runtime/debug.ReadBuildInfo cannot substitute for this: it reports "(devel)"
// for a build that is not driven by `go install module@version`, which is the
// case for the container build.
var (
version = "dev"
commit = "unknown"
buildDate = "unknown"
)

// Version returns the build version. Exported so the HTTP layer can serve it.
func Version() (v, c, d string) { return version, commit, buildDate }

// versionString renders the build information for logs and the -version flag.
func versionString() string {
return fmt.Sprintf("%s (commit %s, built %s, %s/%s, %s)",
version, commit, buildDate, runtime.GOOS, runtime.GOARCH, runtime.Version())
}

func main() {
// Define command line flags, add any other flag required to configure the
// service.
Expand All @@ -33,9 +56,15 @@ func main() {
httpPortF = flag.String("http-port", "", "HTTP port (overrides host HTTP port specified in service design)")
secureF = flag.Bool("secure", false, "Use secure scheme (https or grpcs)")
dbgF = flag.Bool("debug", false, "Log request and response bodies")
versionF = flag.Bool("version", false, "Print version information and exit")
)
flag.Parse()

if *versionF {
fmt.Println(versionString())
return
}

// Setup logger. Replace logger with your own log package of choice.
format := log.FormatJSON
if log.IsTerminal() {
Expand All @@ -46,6 +75,15 @@ func main() {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug logs enabled")
}
// Log the build up front, so a pod can be identified from its logs alone
// without inspecting the image digest.
log.Print(ctx,
log.KV{K: "msg", V: "starting kube-workspaces-api"},
log.KV{K: "version", V: version},
log.KV{K: "commit", V: commit},
log.KV{K: "buildDate", V: buildDate},
log.KV{K: "go", V: runtime.Version()},
log.KV{K: "platform", V: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)})
log.Print(ctx, log.KV{K: "http-port", V: *httpPortF})

// Initialize the services.
Expand Down
9 changes: 9 additions & 0 deletions internal/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ func Middleware(provider *ConfigProvider) func(http.Handler) http.Handler {
return
}

// Always allow the build/version endpoint. Identifying which version
// is deployed is diagnostic information, not privileged, and needing
// a session to read it defeats the purpose during an incident. It
// exposes no cluster state beyond the component image tags.
if r.URL.Path == "/platform/version" {
next.ServeHTTP(w, r)
return
}

// Get auth config
cfg, err := provider.GetConfig(ctx)
if err != nil {
Expand Down