From 397612c14bc9d86b58d880074e7ff008e2ccf5ad Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Sun, 29 Mar 2026 21:59:35 +0500 Subject: [PATCH] [platform] Added dev-overlay for building dev ocirepos Signed-off-by: Myasnikov Daniil --- hack/dev-overlay.md | 65 ++++++++++ hack/dev-overlay.sh | 216 +++++++++++++++++++++++++++++++ packages/core/installer/Makefile | 23 ++++ 3 files changed, 304 insertions(+) create mode 100644 hack/dev-overlay.md create mode 100755 hack/dev-overlay.sh diff --git a/hack/dev-overlay.md b/hack/dev-overlay.md new file mode 100644 index 00000000..ff543ca3 --- /dev/null +++ b/hack/dev-overlay.md @@ -0,0 +1,65 @@ +# Dev Overlay + +Patch the cluster's platform OCI artifact with your local changes without rebuilding or uploading the entire packages tree. + +## Why not `make image-packages`? + +`make image-packages` (from `packages/core/installer/Makefile`) pushes the **entire** `packages/` directory from your current working tree as a new OCI artifact. This has two problems: + +1. **Image version leakage.** Your working tree inherits image versions from the branch you're on (e.g. `cozystack-controller:v1.1.0` on main), which differ from the pinned versions in the cluster's release (e.g. `v1.1.4`). Uploading the whole tree overwrites them all. + +2. **Single-branch only.** If you develop features across multiple branches (branch A changes component A, branch B changes component B), you can only upload from one branch at a time. The old workaround was to create a "frankenstein" branch by merging all feature branches onto a release tag — manual and error-prone. + +## How dev-overlay works + +Instead of replacing the entire artifact, dev-overlay **patches** it with only the files you changed: + +1. Pulls the existing overlay from the registry (or the cluster's base OCI on first run) +2. Runs `git diff origin/main -- packages/` against your working tree to find changed files +3. Copies only those files into the artifact, deletes removed files, cleans up renames +4. Pushes the patched artifact and points the operator at its digest + +Because the diff is against `origin/main` (or a ref you choose), files you didn't touch — including values with pinned image versions — are never overwritten. The cluster keeps its release versions. + +## Accumulation across branches + +Changes from multiple branches stack on top of each other: + +``` +# On branch A (changes component A) +make dev-overlay + +# Switch to branch B (changes component B) +make dev-overlay +``` + +After both runs, the overlay contains changes from both A and B. Files that only A touched are preserved when B runs (B's diff doesn't mention them). If both branches modify the same file, the last applied version wins. + +## Usage + +```bash +cd packages/core/installer + +# Preview what would be applied +make dev-overlay-diff + +# Apply changes (defaults to diffing against origin/main) +make dev-overlay + +# Override the diff base +make dev-overlay DEV_BASE_TAG=some-commit-hash +``` + +## Resetting + +To discard all overlay changes, delete the overlay tag from the registry and restore the operator to the original platform source. There is no automated undo — just point the operator back at the base artifact. + +## How DEV_BASE_TAG affects correctness + +`DEV_BASE_TAG` (default: `origin/main`) is the git ref your changes are diffed against. For image versions to be preserved correctly, it must be a ref where inherited values match your working tree: + +- Your branch has `image: v1.1.0` (inherited from main) +- `origin/main` also has `image: v1.1.0` +- No diff detected — cluster keeps its `v1.1.4` + +If you rebase onto a newer main that bumped a version, `origin/main` tracks that automatically. If you need a fixed point, pass a specific commit hash. diff --git a/hack/dev-overlay.sh b/hack/dev-overlay.sh new file mode 100755 index 00000000..8025e94e --- /dev/null +++ b/hack/dev-overlay.sh @@ -0,0 +1,216 @@ +#!/bin/bash +set -eu + +# Dev overlay: overlay working-tree changes onto a deployed OCI artifact. +# +# Usage: dev-overlay.sh +# +# Required env vars: +# REGISTRY OCI registry to push to +# Optional env vars: +# DEV_BASE_TAG git ref to diff against (default: origin/main) +# DEV_OVERLAY_TAG tag for the overlay artifact (default: dev-overlay) +# OPERATOR_DEPLOY operator Deployment name (default: cozystack-operator) +# OPERATOR_CONTAINER container name in the Deployment (default: $OPERATOR_DEPLOY) +# PLATFORM_OCIREPO OCIRepository resource name (default: cozystack-platform) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" + +DEV_BASE_TAG="${DEV_BASE_TAG:-origin/main}" +DEV_OVERLAY_TAG="${DEV_OVERLAY_TAG:-dev-overlay}" +OPERATOR_DEPLOY="${OPERATOR_DEPLOY:-cozystack-operator}" +OPERATOR_CONTAINER="${OPERATOR_CONTAINER:-$OPERATOR_DEPLOY}" +PLATFORM_OCIREPO="${PLATFORM_OCIREPO:-cozystack-platform}" + +cluster_oci_url() { + kubectl get ocirepository "$PLATFORM_OCIREPO" -n cozy-system \ + -o jsonpath='{.spec.url}' 2>/dev/null || true +} + +resolve_cluster_pull_ref() { + local url="$1" + local tag digest + tag=$(kubectl get ocirepository "$PLATFORM_OCIREPO" -n cozy-system \ + -o jsonpath='{.spec.ref.tag}' 2>/dev/null || true) + digest=$(kubectl get ocirepository "$PLATFORM_OCIREPO" -n cozy-system \ + -o jsonpath='{.spec.ref.digest}' 2>/dev/null || true) + + if [ -n "$tag" ]; then + echo "${url}:${tag}" + elif [ -n "$digest" ]; then + echo "${url}@${digest}" + fi +} + +pull_or_init_workdir() { + local workdir="$1" + local oci_url="$2" + + if flux pull artifact "oci://${REGISTRY}/cozystack-packages:${DEV_OVERLAY_TAG}" \ + --output "$workdir" 2>/dev/null; then + echo "Existing overlay found, accumulating on top" + return + fi + + local base_pull + base_pull=$(resolve_cluster_pull_ref "$oci_url") + if [ -z "$base_pull" ]; then + echo "ERROR: cluster OCIRepository has no tag or digest ref" >&2 + rm -rf "$workdir" + exit 1 + fi + + echo "No overlay yet, pulling base from cluster: $base_pull" + flux pull artifact "$base_pull" --output "$workdir" +} + +apply_branch_changes() { + local workdir="$1" + local f rel + + # Copy changed/added/modified files from working tree + while IFS= read -r f; do + [ -z "$f" ] && continue + rel="${f#packages/}" + mkdir -p "$workdir/$(dirname "$rel")" + cp -r "$REPO_ROOT/$f" "$workdir/$rel" + done < <(git -C "$REPO_ROOT" diff --name-only --diff-filter=ACMR \ + "${DEV_BASE_TAG}" -- packages/) + + # Copy untracked (new) files not yet known to git + while IFS= read -r f; do + [ -z "$f" ] && continue + rel="${f#packages/}" + mkdir -p "$workdir/$(dirname "$rel")" + cp -r "$REPO_ROOT/$f" "$workdir/$rel" + done < <(git -C "$REPO_ROOT" ls-files --others --exclude-standard -- packages/) + + # Delete files removed relative to DEV_BASE_TAG + while IFS= read -r f; do + [ -z "$f" ] && continue + rel="${f#packages/}" + rm -rf "$workdir/$rel" + done < <(git -C "$REPO_ROOT" diff --name-only --diff-filter=D \ + "${DEV_BASE_TAG}" -- packages/) + + # Delete old paths of renamed files to prevent duplicate Helm resources + while IFS=$'\t' read -r _ old_path _; do + [ -z "$old_path" ] && continue + rel="${old_path#packages/}" + rm -rf "$workdir/$rel" + done < <(git -C "$REPO_ROOT" diff --diff-filter=R --name-status \ + "${DEV_BASE_TAG}" -- packages/) +} + +push_artifact() { + local workdir="$1" + local logfile + logfile=$(mktemp) + trap 'rm -f "$logfile"' RETURN + + flux push artifact \ + "oci://${REGISTRY}/cozystack-packages:${DEV_OVERLAY_TAG}" \ + --path="$workdir" \ + --source=dev-overlay \ + --revision="dev:$(git -C "$REPO_ROOT" rev-parse HEAD)" \ + 2>&1 | tee "$logfile" >&2 + + local digest + digest=$(awk -F @ '/artifact successfully pushed/ {print $2}' "$logfile") + if [ -z "$digest" ]; then + echo "ERROR: could not parse digest from push output" >&2 + exit 1 + fi + echo "$digest" +} + +patch_operator_ref() { + local ref="$1" + local url="$2" + + local jq_filter + jq_filter=$(cat <<'JQ' + .spec.template.spec.containers |= map( + if .name == $container then + .args |= map( + if startswith("--platform-source-url=") then "--platform-source-url=" + $url + elif startswith("--platform-source-ref=") then "--platform-source-ref=" + $ref + else . end) + else . end) +JQ + ) + + kubectl get deploy "$OPERATOR_DEPLOY" -n cozy-system -o json \ + | jq --arg ref "$ref" \ + --arg url "$url" \ + --arg container "$OPERATOR_CONTAINER" \ + "$jq_filter" \ + | kubectl apply -f - +} + +cmd_diff() { + local oci_url base_pull + oci_url=$(cluster_oci_url) + base_pull=$(resolve_cluster_pull_ref "$oci_url") + : "${base_pull:=${oci_url}:${DEV_BASE_TAG}}" + + echo "Base: ${DEV_BASE_TAG} Branch: $(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" + echo "Pull from: $base_pull" + echo "Push to: oci://${REGISTRY}/cozystack-packages:${DEV_OVERLAY_TAG}" + + local probe + probe=$(mktemp -d) + if flux pull artifact "oci://${REGISTRY}/cozystack-packages:${DEV_OVERLAY_TAG}" \ + --output "$probe" 2>/dev/null; then + rm -rf "$probe" + echo "Existing overlay found, changes will be layered on top" + else + rm -rf "$probe" + echo "No overlay exists yet, $base_pull will be used as starting point" + fi + + echo "" + echo "=== Changed files (git diff) ===" + git -C "$REPO_ROOT" diff --stat "${DEV_BASE_TAG}" -- packages/ + + local untracked + untracked=$(git -C "$REPO_ROOT" ls-files --others --exclude-standard -- packages/) + if [ -n "$untracked" ]; then + echo "" + echo "=== Untracked new files ===" + echo "$untracked" + fi + + echo "" + git -C "$REPO_ROOT" diff "${DEV_BASE_TAG}" -- packages/ +} + +cmd_apply() { + local oci_url + oci_url=$(cluster_oci_url) + + local workdir + workdir=$(mktemp -d) + trap "rm -rf '$workdir'" EXIT + + pull_or_init_workdir "$workdir" "$oci_url" + apply_branch_changes "$workdir" + + local digest + digest=$(push_artifact "$workdir") + + echo "Updating Deployment ${OPERATOR_DEPLOY} url=oci://${REGISTRY}/cozystack-packages ref=digest=${digest}" + patch_operator_ref "digest=${digest}" "oci://${REGISTRY}/cozystack-packages" + + echo "Operator restarting — watch: kubectl rollout status deploy/${OPERATOR_DEPLOY} -n cozy-system" +} + +case "${1:-}" in + diff) cmd_diff ;; + apply) cmd_apply ;; + *) + echo "Usage: $0 " >&2 + exit 1 + ;; +esac diff --git a/packages/core/installer/Makefile b/packages/core/installer/Makefile index a661bed4..8bf7b930 100644 --- a/packages/core/installer/Makefile +++ b/packages/core/installer/Makefile @@ -50,3 +50,26 @@ chart: PKG=$$(helm package . --version $(COZYSTACK_VERSION) | awk '{print $$NF}'); \ trap 'rm -f "$$PKG"' EXIT; \ if [ "$(PUSH)" = "1" ]; then helm push "$$PKG" oci://$(REGISTRY); fi + +# === Dev overlay targets === +# See hack/dev-overlay.sh for full documentation and implementation. + +DEV_BASE_TAG ?= origin/main +DEV_OVERLAY_TAG ?= dev-overlay +OPERATOR_DEPLOY ?= cozystack-operator +OPERATOR_CONTAINER ?= $(OPERATOR_DEPLOY) +PLATFORM_OCIREPO ?= cozystack-platform + +DEV_OVERLAY_ENV = \ + REGISTRY="$(REGISTRY)" \ + DEV_BASE_TAG="$(DEV_BASE_TAG)" \ + DEV_OVERLAY_TAG="$(DEV_OVERLAY_TAG)" \ + OPERATOR_DEPLOY="$(OPERATOR_DEPLOY)" \ + OPERATOR_CONTAINER="$(OPERATOR_CONTAINER)" \ + PLATFORM_OCIREPO="$(PLATFORM_OCIREPO)" + +dev-overlay-diff: ## Show what files would be added/removed by dev-overlay (dry run, no changes) + @$(DEV_OVERLAY_ENV) ../../../hack/dev-overlay.sh diff + +dev-overlay: ## Overlay current branch's changes onto deployed artifact, update operator ref + @$(DEV_OVERLAY_ENV) ../../../hack/dev-overlay.sh apply