Feature/4.0/helm installer#1064
Open
jorgemoralespou wants to merge 196 commits into
Open
Conversation
Adds the v4 architecture-of-record under docs/architecture/:
- educates-current-state.md — what v3 actually is today.
- educates-v4-development-plan.md — phased plan + open items + the
pre-phase chart workstream this commit-set lands.
- educates-crd-draft-v1alpha1-r3.md — operator CRD design (informs
Phase 0+).
- decisions.md — append-only decisions log; entries grouped by
topic with reconsider triggers where relevant.
CLAUDE.md is the briefing for any future Claude Code session in this
repo: scope of v4 vs v3, what's safe to touch, working norms,
references back to the architecture docs.
.gitignore picks up .claude/ so user-local agent state doesn't leak.
Pre-phase deliverable from docs/architecture/educates-v4-development-plan.md.
The chart is the canonical Helm install for the Educates v4 runtime —
what users helm install today (and what the v4 operator will install
on their behalf in Phase 4).
Layout:
installer/charts/educates-training-platform/
├── Chart.yaml, values.yaml, .helmignore (umbrella)
└── charts/
├── secrets-manager/ (CRDs + operator: cross-NS secret
│ propagation primitives)
├── lookup-service/ (federation API; off by default)
├── remote-access/ (read-only RBAC + token Secret for
│ external CLI clients; toggleable
│ independently)
└── session-manager/ (workshop runtime + bundled v3
Kyverno policies)
CRDs ship in each subchart's `crds/` directory rather than templates/
because Helm can't apply CRDs and CRs of those CRDs in a single
release otherwise (see decisions.md).
session-manager ships v3-vendored Kyverno policies on two paths:
- bundledKyvernoPolicies.clusterPolicies — Pod Security Standards
profiles installed cluster-wide as ClusterPolicy resources.
- bundledKyvernoPolicies.workshopPolicies — operational policies +
Educates-internal `require-ingress-session-name`, written into
the educates-config Secret for session-manager to clone per
workshop environment.
additionalKyvernoPolicies.{clusterPolicies,workshopPolicies} let
admins extend either bundle through chart values — net-new vs v3,
which required out-of-band kubectl-apply.
Image refs default to ghcr.io/educates/educates-* (v3 naming
convention). Tag default is the chart's appVersion; scenarios pin to
3.7.1 because no v4 runtime images exist yet.
…lates Stray file from troubleshooting .Files.Glob behaviour in kyverno-cluster-policies.yaml during chart development. Helm treats templates/_*.txt as a partial-style include and skips it during render, so it never affected output — but it shouldn't be in the chart.
Six end-to-end scenarios under installer/charts/educates-training-platform/tests/.
Each scenario provisions a kind cluster (cluster-only), runs an
optional pre-install hook to stage cluster-side fixtures, applies the
chart, runs an optional post-install hook before rollout-status, and
exercises the deploy-workshop / browse path.
run-scenario.sh wires four hook points (pre-install, post-install,
post-deploy, teardown) so each scenario carries its own setup +
assertions. .env loading + envsubst rendering of scenario files lets
the runner pick up DOMAIN, TLS_CERT_PATH, CA_CERT_PATH from the user
shell (handy with mkcert auto-detection) without touching scenario
files.
Scenarios:
01-local-http-nip-io — minimal HTTP smoke; everything
optional off.
02-kind-tls-wildcard — offline-generated wildcard +
secretPropagation.upstream paths.
03-kind-cert-manager-issuer — cert-manager + certs package
issues the wildcard from a
user-provided CA.
04-website-theme — custom websiteTheme reaches the
live portal HTML (post-deploy
curls portal URL, greps marker).
05-image-pull-secrets — auth'd local registry serves a
copy of educates-session-manager;
scenario fails if the chart's
pull-secret chain breaks.
06-additional-kyverno-policies — bundled + user-supplied
ClusterPolicies on both paths
(cluster-wide + per-environment).
Step 5 prints a click-through portal URL with URL-encoded password
(uses a pure-bash percent-encoder).
Add the docs-of-record for the session-manager subchart's typed values shape (values.yaml + JSON schema), and update the v4 development plan to mark Kyverno-policy bundling as done and rescope the typed-runtime- config follow-up against the broader target shape.
Refactor the session-manager subchart's values surface from an opaque
config blob plus ad-hoc toggles into the typed shape defined by the
docs-of-record (docs/architecture/session-manager-chart-values{.yaml,
-schema.json}). Adds values.schema.json so Helm catches shape errors
at template time.
Key changes:
- clusterIngress / clusterSecurity / workshopSecurity replace the
config: clusterIngress + bundledKyvernoPolicies + openshift.enabled
knobs. policyEngine and rulesEngine are PascalCase enums in chart
values, lowercased when emitted into the runtime config blob to
match the runtime's existing expectations (no runtime change).
- Promotes imageRegistry, imageVersions, sessionCookies, clusterStorage,
clusterRuntime, clusterNetwork, dockerDaemon, workshopAnalytics, and
websiteStyling.{defaultTheme,frameAncestors} out of config into typed
values; chart auto-injects operator.namespace (release ns) and
version (chart appVersion).
- websiteStyling.inline.{workshopDashboard,workshopInstructions,
workshopStarted,workshopFinished,trainingPortal} replaces the flat
websiteTheme map; the chart maps the structured triples to the
flat secret keys (.html / .js / .css) the runtime expects.
- SecretCopier rules for ingress TLS+CA are auto-derived from
clusterIngress.{tls,ca}CertificateRef.namespace; the explicit
secretPropagation.upstream.ingressTLS / ingressCA knobs are gone.
themeDataRefs entries with non-local namespaces are also auto-copied.
- Materialises empty-string TLS/CA refs in the operator-config blob
even when unset, papering over the runtime's xget-no-default-None
quirk.
- config remains as an opaque escape hatch, deep-merged on top of the
typed-derived blob so users can land new fields before promotion.
Scenarios 01 (local HTTP nip.io) and 02 (kind + TLS wildcard) are
updated to the typed shape and verified via helm template. The
remaining scenarios (03-06) move to the typed shape in a follow-up
commit.
Brings scenarios 03-06 onto the typed values shape introduced in the
previous commit and adds scenario 07 to exercise the `config:` escape-
hatch deep-merge.
- 03 (kind cert-manager issuer): cross-namespace TLS/CA refs now drive
the SecretCopier auto-derive instead of an explicit
`secretPropagation.upstream.ingressTLS/ingressCA` block.
- 04 (website theme): uses `websiteStyling.inline.trainingPortal.{html,
style}`, exercising the chart's structured-triple → flat-secret-key
mapping (`style` → `.css`).
- 05 (image-pull secrets): typed top-level `imagePullSecrets` for the
PodSpec; `secretPropagation.imagePullSecretNames` and
`secretPropagation.upstream.imagePullSecrets` unchanged.
- 06 (additional Kyverno policies): toggles renamed —
`clusterSecurity.{policyEngine,additionalKyvernoPolicies}` and
`workshopSecurity.{rulesEngine,additionalKyvernoPolicies}` replace
the old `bundledKyvernoPolicies` / `additionalKyvernoPolicies.{cluster,
workshop}Policies` blocks.
- 07 (new): asserts the `config:` opaque map deep-merges on top of the
typed-derived runtime config and wins on conflict (`dockerDaemon.
networkMTU` override) and passes through unknown fields untouched
(`experimental.markerKey`).
Drive-by: schema's top-level `imagePullSecrets` was [string] but the
PodSpec wants the standard k8s [{name: ...}] shape — fixed in both the
chart's values.schema.json and the docs-of-record.
Records the decision behind the typed-values refactor (commits a57ef864 / d339c016 / f61d3c8e): a single typed surface serves both the operator and standalone chart users, with `config:` retained as an escape hatch for not-yet-promoted runtime fields. The earlier "operator-driven, not v3-driven" decision is superseded — its framing left standalone users writing opaque YAML against the v3 schema from memory, which contradicted the chart's own publish-as-canonical-Helm install positioning.
…default imageVersions
Fixes ErrImagePull on training-portal (and any other runtime-spawned
child image) by stopping the chart from auto-injecting Chart.AppVersion
(`4.0.0-alpha.1`) as the runtime config's `version` field. Restores the
defaulted `imageVersions` list that v3's carvel installer rendered out
of `config/images.yaml`, which the previous refactor silently dropped.
- New typed value `runtimeVersion: "3.7.1"` drives:
* the chart-pod `image.tag` default
* the `imagePuller.pauseImage.tag` default
* the `version` field auto-injected into the operator-config blob
- `imageVersions` defaults to the full v3 list: 12 Educates-published
images pinned to `runtimeVersion`, 7 upstream pins (docker-in-docker,
loftsh-kubernetes-v1.31..34, loftsh-vcluster, debian-base-image) at
their v3-vendored tags. Visible in values.yaml so chart users see
exactly which images the runtime can pull.
- Drops the redundant `image.tag: "3.7.1"` overrides in scenarios 01-07
(chart default now resolves correctly). Scenario 05 keeps its
`image.repository` override for the auth'd local registry but no
longer pins the tag.
- Mirrored in docs-of-record (values.yaml + JSON schema), the v4 dev
plan, and a new decisions.md entry covering the rationale and the
documented two-place-edit when bumping `runtimeVersion`.
…helper-defaulted imageVersions with per-key merge Supersedes the runtimeVersion + populated-values approach from e71e41af with a more idiomatic shape: - `Chart.appVersion` IS the runtime image version. Set to "3.7.1" across the umbrella + four subchart Chart.yamls (was "4.0.0-alpha.1" — which doesn't exist as a published image and was the original source of the ErrImagePull). `Chart.version` stays at 4.0.0-alpha.1 (the chart-package version). They normally move together at release time; the field separation exists for chart-only patches. - Removed `runtimeVersion` typed value. Image-tag defaults (`session-manager.image.tag`, `imagePuller.pauseImage.tag`) and the operator-config blob's `version` field all source `Chart.appVersion` directly. - The full default `imageVersions` set moves into a new template helper `session-manager.imageVersions` (mirroring v3's carvel-installer images.yaml). Educates-published entries derive their tag from `Chart.appVersion`; upstream pins (docker-in-docker, loftsh-*, debian-base-image) are hard-coded. - User-supplied `.Values.imageVersions` entries merge BY NAME on top of the helper defaults: an override replaces just the matching default's image, other defaults pass through, names not in the default list are appended (forward-compat). Strictly better UX than v3's full-list replacement; chart users override only what they need. - `values.yaml`, `values.schema.json`, and the docs-of-record return to `imageVersions: []` with comments documenting the per-key merge semantics. The helper is the documented inventory. - Dev plan updated. decisions.md entry replaced with the corrected rationale (Chart.appVersion sourcing, helper-defaulted list, per-key merge UX).
…-pod, pause, and runtime children
A chart user pointing at a fork or a local registry now redirects
every Educates-image reference with one knob. Previously only the
runtime-spawned children were derivable from imageRegistry — the
chart pod and the pause image were hard-coded to ghcr.io/educates/...,
which broke the dev workflow against a fork.
- `imageRegistry.host` / `.namespace` default to `ghcr.io` / `educates`
(was empty/empty). They now compose the prefix used by:
* `session-manager.imageRegistryPrefix` helper (new)
* the chart-pod `image.repository` default (when empty)
* the pause image `imagePuller.pauseImage.repository` default
(when empty)
* the Educates-published entries in the `session-manager.imageVersions`
helper
- Upstream pins (docker-in-docker, loftsh-*, debian-base-image) are NOT
relocated by imageRegistry — they're public upstream images that don't
follow the educates-<name> naming convention. Mirror them via per-entry
imageVersions overrides instead.
- `image.repository` and `imagePuller.pauseImage.repository` defaults
flip from hard-coded refs to empty strings; helpers `session-manager.
image.repository` and `session-manager.pause.image.repository` resolve
the empty-derives-from-imageRegistry behaviour. Schema's `imageRef`
drops the minLength on `repository` to allow the empty.
- Helper bails fast (`fail`) if imageRegistry.host is empty — the
chart can't compose a default ref without it.
- Verified end-to-end: setting `imageRegistry: {host: localhost:5001,
namespace: educates-fork}` redirects 12 runtime children + chart pod
+ pause to that prefix, while upstream pins stay put.
Mirrored in docs-of-record (values.yaml + JSON schema), v4 dev plan,
and the prior decisions.md entry.
…curity to umbrella globals
Cross-cutting deployment-scope values now live at the umbrella under
`global:`. Helm propagates them to every subchart as `.Values.global.<key>`.
Subcharts retain a local block of the same name with sensible defaults;
new helpers deep-merge the umbrella global over the subchart local, with
globals winning per-leaf where set. Subcharts remain independently
installable.
Concretely:
- session-manager: new `resolved{ImageRegistry,ClusterIngress,
ClusterSecurity}` helpers feed `imageRegistryPrefix`, `derivedProtocol`,
`operatorConfigYAML`, `kyverno-cluster-policies.yaml`,
`clusterrolebindings.yaml`, and `secretcopiers.yaml`. Schema drops
`clusterIngress` and `clusterSecurity` from `required` and removes the
subchart-local `clusterIngress.domain` minLength — helpers do the
post-merge `fail` instead.
- lookup-service: new `imageRegistry` block (default ghcr.io/educates),
`image.repository` flips to empty (derives from imageRegistry),
helpers added (`resolvedImageRegistry`, `imageRegistryPrefix`,
`image.repository`). Wired into the Deployment. New
`values.schema.json`.
- secrets-manager: same image-helper additions. `openshift.enabled`
removed; the SCC ClusterRoleBinding now gates on
`clusterSecurity.policyEngine == "OpenShiftSCC"` (resolved from
globals when present). New `values.schema.json`.
- Umbrella `values.yaml` gains a `global:` block with commented examples
for the three keys.
- All seven scenarios converted to canonical-globals shape: cross-
cutting values live under `global:`, subchart blocks shrink to per-
subchart concerns only. Verified end-to-end — setting
`global.imageRegistry.{host,namespace}` redirects all chart pods +
runtime children; `global.clusterSecurity.policyEngine: OpenShiftSCC`
triggers SCC bindings in BOTH session-manager and secrets-manager.
Mirrored in the doc-of-record (note about dual-source pattern), the v4
dev plan (each cross-cutting block now flagged as a global), and a new
decisions.md entry covering the rationale and trade-offs.
Drive-by: `tests` added to .helmignore so scenario fixtures don't ship
with the chart package.
… render ca-trust-store init container
Drops lookup-service's specialised `caTrust` block and `ingress.tls`
field in favour of consuming `global.clusterIngress` (with subchart-
local fall-back). The lookup-service Ingress's TLS Secret now derives
from the resolved `clusterIngress.tlsCertificateRef.name` (typically
the wildcard cert covering `*.<domain>`), and the chart renders a
ca-trust-store init container when `clusterIngress.caCertificateRef.name`
is set.
- New `clusterIngress` block in lookup-service values.yaml mirrors the
shape introduced in session-manager + the umbrella global.
- `caTrust.{secretName,initImage}` removed; the init image is no longer
base-environment but the lookup-service main image itself (Fedora-
based: has `update-ca-trust` and `tar`). Zero extra image pulls; the
kubelet already has it on the node. Mirrors v3's
overlay-ca-injector.yaml mechanism without the cost of pulling a
multi-GB workshop image.
- `ingress.tls.secretName` removed; the Ingress derives TLS from the
resolved `clusterIngress.tlsCertificateRef`.
- New `secretcopiers.yaml` auto-derives copy rules for both the TLS
Secret and the CA Secret when their refs target a foreign namespace.
Renders independently of session-manager's SecretCopier so this chart
is installable standalone; under the umbrella both subcharts render
their own rules (idempotent — same source-Secret copied once
regardless of how many rules reference it).
- Helpers updated: drop `caTrust.image.{tag,pullPolicy}`, add
`resolvedClusterIngress` and `caTrustEnabled`.
- Schema reflects the new shape.
Verified by enabling lookup-service against a TLS+CA scenario:
- Ingress emits `tls: [{secretName: wildcard-tls}]` with the
hostname-specific `host` and the resolved cert name.
- Init container reuses the lookup-service image and runs
`update-ca-trust && tar -C /etc/pki/ca-trust ...`.
- Main container mounts the CA-populated trust store at
/etc/pki/ca-trust read-only.
- SecretCopier `educates-lookup-service-ingress-secrets` pulls both
refs into the release namespace.
…ger Deployment Mirrors what step 2 added to lookup-service: a chart-side ca-trust-store init container that builds a CA-populated trust store from `global.clusterIngress.caCertificateRef` and the main container mounts the result at /etc/pki/ca-trust read-only. Reuses the main session- manager image (Fedora-based: has `update-ca-trust` and `tar`) so no extra image pull on the node. v3 only injected the trust store into lookup-service. Including it in session-manager too is harmless when the CA isn't needed and avoids debugging "why does X fail TLS verify?" later if session-manager ever gains code paths that reach external TLS endpoints fronted by the private CA. - New `session-manager.caTrustEnabled` helper + Deployment template rewire — initContainers / volumes / volumeMount conditionally on the resolved `clusterIngress.caCertificateRef.name`. - The init container's securityContext explicitly sets `runAsNonRoot: false` to override the pod-level `runAsNonRoot: true` enforcement (the trust-store update needs UID 0 to write /etc/pki/ca-trust). Mirrored in lookup-service for consistency. - Verified: scenario 02 (TLS+CA) renders the init container; scenario 01 (no CA) does not; existing SecretCopier auto-derive still pulls the CA Secret into the release namespace where the init container consumes it.
…chart Brings remote-access in line with the schema discipline applied to the other three subcharts (additionalProperties: false; only `enabled` and the Helm-injected `global` are valid). The subchart has no configurable knobs in v0.1.0 — the schema serves purely as a typo-catcher and a contract that future additions are deliberate. All seven scenarios still render; an additional smoke-render with remote-access.enabled=true also passes.
Restores the v3 cluster-node CA injection feature as a sibling subchart under the umbrella, replacing the never-rendered `session-manager.clusterIngress.caNodeInjector.enabled` stub. Toggle is the umbrella's `node-ca-injector.enabled: false` (default off, opt-in) via Helm's standard subchart-condition mechanism. What renders when enabled (mirroring v3's 07-node-ca-injector.yaml): - ServiceAccount + ClusterRole/Binding (Ingress watch) + Role/RoleBinding (ConfigMap manage in release ns). - `node-ca-injector-controller` Deployment running the `controller` subcommand — watches Ingresses, builds the `educates-registry-hosts` ConfigMap. - `node-ca-injector` DaemonSet running the `sync` subcommand — privileged, mounts the CA Secret + hosts ConfigMap + hostPath `/etc/containerd/certs.d`. Writes per-host containerd registry-CA configuration so containerd trusts the cluster's private CA when pulling images. - SecretCopier auto-derived when the CA ref's namespace is foreign. The subchart consumes `global.clusterIngress.caCertificateRef` (with subchart-local fall-back for standalone) and fails fast at template time if the resolved CA ref is empty. Image is derived from `global.imageRegistry` so the same fork/local-registry knob redirects this subchart too. Has its own `values.schema.json`. Relationship to the per-pod ca-trust-store init container (steps 2/3): complementary, not overlapping. Init container handles in-pod TLS verify for our own Deployments; node-ca-injector handles container-runtime- level trust for image pulls (including pulls performed by pods we don't render — third-party operators, docker-in-docker workshop sessions). Both keyed on the same global CA ref; both independently togglable. `session-manager.clusterIngress.caNodeInjector.enabled` is removed from values.yaml + values.schema.json + the doc-of-record. New decisions.md entry covers the rationale + the relationship between the two CA-trust mechanisms.
… top-level shape
Closes the validation gap on the umbrella's cross-cutting `global:`
block. Subchart schemas correctly treat `global` as opaque (they
shouldn't dictate the umbrella's contract), which meant typos like
`global.clusterSecuirty.policyEngine` or `global.imageRegistry.namespece`
silently fell through — every subchart fell back to its local defaults
and the user's intended override was lost.
The umbrella schema:
- Validates the `global.{imageRegistry,clusterIngress,clusterSecurity}`
shape with `additionalProperties: false` at every level.
- Forbids unknown top-level keys (catches misspelled subchart names
like `sesion-manager:` that Helm would otherwise treat as inert).
- Treats each subchart block (`secrets-manager`, `lookup-service`,
`remote-access`, `session-manager`, `node-ca-injector`) as
`{ "type": "object" }` and delegates detailed validation to that
subchart's own schema. No duplication.
Verified: all three classes of typo (misspelled global key, misspelled
global nested field, unknown top-level key) trigger a clear schema
error at `helm template` time. All seven existing scenarios still
render cleanly.
decisions.md entry covers the split rationale.
End-to-end test for node-ca-injector: a workshop session builds a tiny
container image, pushes it to the per-session registry (HTTPS via the
wildcard cert), then creates a Deployment that pulls from that
registry. Successful rollout is the proof — without the cluster CA in
containerd's per-host trust under /etc/containerd/certs.d/, the pull
would fail with TLS verify errors and the rollout would hang.
Runner change:
- run-scenario.sh now detects `<scenario>/workshop/resources/workshop.yaml`
and, if present, publishes the scenario-local workshop to the local
registry stood up by `educates local cluster create` (localhost:5001)
via `educates publish-workshop <scenario>/workshop`, then deploys it
with `educates deploy-workshop -f <that path>`. Scenarios 01-07 keep
the existing default-WORKSHOP_URL behaviour because they don't ship
their own workshop directory.
Scenario 08 (`08-node-ca-injector-image-pull`):
- educates-config.yaml + pre-install.sh: identical to scenario 02
(kind + Contour + Kyverno; pre-install materialises a wildcard TLS
Secret + CA Secret in `educates-secrets`).
- chart-values.yaml: scenario-02 globals shape plus
`node-ca-injector.enabled: true` at the umbrella.
- description.md: explains what the test demonstrates and what success
looks like.
- workshop/: a complete `lab-node-ca-pull` workshop. Enables `docker`
and `registry` session applications. Four content pages walk the user
through writing a Dockerfile, building, tagging and pushing to
`${REGISTRY_HOST}`, creating the Deployment, and watching
`kubectl rollout status` succeed. The summary calls out which step is
the actual proof of node-ca-injector working.
Verified all eight scenarios render cleanly under `helm template`;
scenario 08 emits all eight node-ca-injector resources plus the chart
pods with the ca-trust-store init container.
The runner pause at step 5/6 is the verification surface — interactive
since the proof lives in a workshop session, not in a kubectl assertion
the runner can make against the cluster directly.
…me defaults via Chart.yaml annotations
The user-facing image-registry knob is renamed to
`development.imageRegistry` (subchart-local) and
`global.development.imageRegistry` (umbrella), and the publish-time
default registry moves from a populated `values.yaml` block to
Chart.yaml annotations:
educates.dev/image-registry-host: "ghcr.io"
educates.dev/image-registry-namespace: "educates"
The release workflow updates these annotations per fork (one `yq -i`
call per Chart.yaml) so the chart that gets shipped points at the
right registry without a values override. Mirrors v3's
`push-installer-bundle` Makefile target which baked refs at OCI-bundle
build time, translated to a chart-publish edit step.
The runtime IMAGE_REPOSITORY semantic now matches v3's intent:
- When `development.imageRegistry` is set: emitted into the runtime
config so workshop sessions get IMAGE_REPOSITORY={host}/{namespace}
for `$(image_repository)` content placeholder resolution.
- When empty (normal use): runtime config's `imageRegistry` block is
emitted empty; runtime falls back to
`registry.default.svc.cluster.local` per
`session-manager/handlers/operator_config.py:35`. This avoids
silently breaking the local-dev workflow on installs that left a
populated registry in place.
Implementation notes:
- Each subchart helper now has TWO resolvers:
* `resolvedImageRegistry` falls back to Chart.yaml annotations and
is consumed by chart-rendered + runtime-children image-ref
composition.
* `resolvedDevelopmentImageRegistry` (session-manager only — the
subchart that owns the operator-config emission) does NOT fall
back to annotations; returns user/global only. Emitted into the
runtime config blob's `imageRegistry` field.
- Annotations added to all four image-rendering Chart.yamls (session-
manager, lookup-service, secrets-manager, node-ca-injector). Helper
reads `.Chart.Annotations["educates.dev/image-registry-..."]`.
- Subchart `values.yaml`: `imageRegistry` block dropped; replaced by
empty `development.imageRegistry`. Schemas updated. Doc-of-record
follows the session-manager subchart shape.
- Umbrella `values.yaml` and schema: `global.imageRegistry` →
`global.development.imageRegistry`.
- Helper failure message updated to point at both override paths.
Verified end-to-end:
- Normal mode (scenario 01 with empty development.imageRegistry):
chart pods resolve to `ghcr.io/educates/educates-{secrets,session}-
manager:3.7.1` from annotations; runtime config blob has
`imageRegistry: { host: "", namespace: "" }`.
- Dev override (`global.development.imageRegistry: { host:
localhost:5001, namespace: educates-dev }`): all 12 Educates
imageVersions entries redirect to localhost:5001/educates-dev/
AND the runtime config blob carries the same registry, so workshops
with `$(image_repository)` placeholders resolve consistently.
decisions.md gets a new entry superseding the prior `imageRegistry`
decision with the development-knob framing and the rationale for the
two-resolver split.
Captures GitHub-issue drafts for runtime simplifications that should land once the v4 chart-based install ships in develop. Format mirrors decisions.md — one heading per issue, prose body, date added — so entries can be transcribed to the issue tracker with minimal further editing. Initial entries: - Simplify `operator_config.py` IMAGE_REPOSITORY resolution. Drop the `imageRegistry.host` + `imageRegistry.namespace` compose logic in favour of a single `imageRepository` field, and stop falling through in `image_reference()` for short-names not in `imageVersions` — treat them as config errors instead. - Drop `clusterIngress.tlsCertificate` / `caCertificate` inline forms from `operator_config.py`. The chart only emits the `*Ref` forms. - CI lint: assert Chart.yaml annotations stay in sync across all four image-rendering subcharts (and optionally that version / appVersion / dependency versions match across umbrella + subcharts). - Document the chart release workflow's annotation update step in the release runbook. Each entry has a "trigger to file" so it doesn't get filed prematurely while v3 is still the production install path.
Tighten Phase 0 in the v4 development plan and add three decisions-log entries covering the choices made for kubebuilder bootstrap: - Operator at installer/operator/, kubebuilder's config/ kustomize tree stripped; controller-gen writes CRDs and RBAC directly into the educates-installer Helm chart. - Spec types adopt the full r3 shape from Phase 0; status grows alongside the reconciler that produces each field. Avoids dead API surface drifting from r3. - Operator image at Phase 0 is a local-dev placeholder built via make docker-build; publish-time annotations and release workflow are deferred to Phase 6. Also narrows Phase 0 CEL scope to singleton-name + mode-immutability (mode-field exclusivity moves to Phase 1) and Phase 0 RBAC to the four CRDs only (referenced-resource watches move to Phase 1). CLAUDE.md gets a new "Operator project (Phase 0+)" block listing the make targets and conventions.
Phase 0 step 1: bare kubebuilder scaffold at installer/operator/. No
real types or reconciler logic — just the layout we'll grow.
- Multigroup project (config + platform groups under domain
educates.dev), repo path
github.com/educates/educates-training-platform/installer/operator,
added to root go.work.
- Four APIs scaffolded with controller stubs: EducatesClusterConfig
(config/v1alpha1), SecretsManager / LookupService / SessionManager
(platform/v1alpha1).
- Per the Phase 0 layout decision, kubebuilder's config/ kustomize
tree is stripped; controller-gen writes CRDs and RBAC into
bin/manifests/{crd,rbac} for now and will retarget the
educates-installer chart in step 5.
- Makefile pruned of kustomize-dependent targets (install, uninstall,
deploy, undeploy, build-installer, kustomize tool target,
setup-test-e2e/test-e2e, docker-buildx, docker-push) and the
kubebuilder-default test/e2e/ tree removed. smoke-test target is
staged with a fail-fast message until step 5 wires it.
- Operator-local .github/workflows/ removed; the monorepo CI workflow
for the operator lands in step 6.
Verified: go build ./..., go vet ./..., make generate, and make
manifests all run clean. CRDs + RBAC YAML produced for all four kinds.
Translate the full r3 EducatesClusterConfig spec surface into Go types under api/config/v1alpha1/. Mirrors the CRD draft revision 3 in docs/architecture/educates-crd-draft-v1alpha1-r3.md: - Mode (Managed | Inline), with the full Managed-mode tree: Infrastructure (provider + optional cloud + service-account identities), Ingress (domain, ingressClassName, controller, certificates), Certificates (BundledCertManager / ExternalCertManager / StaticCertificate), ACME with DNS01 solvers (Route53, CloudDNS, Cloudflare, AzureDNS), DNS (BundledExternalDNS / Manual / None), PolicyEnforcement (clusterPolicy, workshopPolicy, kyverno), ImageRegistry (prefix + pullSecrets). - Inline-mode tree mirroring the same surface where applicable (ingress, policyEnforcement, imageRegistry). - Shared OperationalBlock duplicated at every Bundled use site per the r3 design intent (no schema-ref factoring). - Static defaults marked with +kubebuilder:default for fields the r3 doc calls out: dns.provider=None, clusterPolicy.engine=Kyverno, workshopPolicy.engine=Kyverno, kyverno.provider=Bundled. - Enum validation on every closed-set field via +kubebuilder:validation:Enum. Phase 0 CEL rules added (the only two in scope; mode-field exclusivity moves to Phase 1 per the development plan): - Singleton-name on the wrapper type: self.metadata.name == 'cluster' - Mode immutability on the spec: self.mode == oldSelf.mode Status surface is intentionally minimal (observedGeneration, phase, conditions) per the "status grows alongside reconcilers" decision. CRD shape is now Cluster-scoped with shortName ecc and Mode/Phase/Age printer columns. controller-gen output verified: scope: Cluster, both CEL rules present, four defaults populated, three printer columns, ~1.2k lines of well-formed YAML. go build, go vet, make generate, make manifests all pass.
…rom r3 Translate the three platform-group CRDs from r3 into Go types under api/platform/v1alpha1/. Mirrors the CRD draft revision 3: - SecretsManager: image override, logLevel (default info), resources. No replicas knob (singleton at the pod level upstream). Image-pull credentials inherit from EducatesClusterConfig.status.imageRegistry. - LookupService: ingress (prefix + optional tlsSecretRef override), image, logLevel (default info), resources. Component-specific knobs (auth, rate-limiting, storage) deferred until the lookup-service owner specifies them. - SessionManager: ingressOverrides, workshopPolicyOverride, images (overrides only — registry prefix + pullSecrets inherit from EducatesClusterConfig.status), themes (ConfigMap/Secret/URL source type), defaultTheme, tracking (Google Analytics, Amplitude, Clarity, webhook), defaultAccessCredentials, sessionCookieDomain, allowedEmbeddingHosts, storage, network (packetSize, blockedCidrs), imageCache (default disabled), registryMirrors, logLevel. Shared types in common_types.go: LogLevel, ComponentPhase, LocalObjectReference, ImageRef. WorkshopPolicyEngine is duplicated in sessionmanager_types.go to avoid coupling the platform package to the config API group. All three CRDs Cluster-scoped with singleton-name CEL (self.metadata.name == 'cluster') and Phase/Age printer columns. Status surface intentionally minimal (observedGeneration, phase, conditions) per the Phase 0 status policy in decisions.md. go build, go vet, make generate, make manifests all pass. CRDs render clean (lookup ~257, secrets ~234, session ~431 lines).
…ines
Phase 0 step 5: stand up the educates-installer Helm chart and wire the
four trivial reconcilers. The chart is now the canonical artefact for
the v4 installer; controller-gen targets it directly per the Phase 0
layout decision.
Chart at installer/charts/educates-installer/:
- Chart.yaml apiVersion v2, kubeVersion >=1.31.0-0, version and
appVersion locked at 4.0.0-alpha.1 (matches the runtime chart's
versioning approach but tracks operator releases independently).
- crds/: the four CRDs from controller-gen, in Helm's reserved
location — installed once on first helm install, not templated, not
deleted on uninstall (mirrors the runtime chart's CRD-shipping
decision).
- templates/rbac/role.yaml: ClusterRole "educates-installer-manager"
generated by controller-gen. Phase 0 RBAC scope is exactly the four
CRDs and their /status + /finalizers — no Secrets/ClusterIssuers/
IngressClasses watches yet (those land in Phase 1 with the Inline
validator).
- templates/rbac/role-binding.yaml, serviceaccount.yaml,
deployment.yaml: hand-written, Helm-templated. Deployment runs the
manager binary with --health-probe-bind-address=:8081, metrics off
by default, leader election off (single replica).
- values.yaml: image as repository + tag (dev placeholder),
imagePullSecrets, resources, nodeSelector, tolerations, affinity,
leaderElection.enabled. Comment block in values.yaml documents the
Phase 0 local-dev workflow (make docker-build + kind load + helm
install).
- NOTES.txt: post-install message naming the four CRDs, calling out
the Phase 0 stub-only state, and listing useful kubectl commands.
Operator changes:
- All four Reconcile() bodies emit a single "Reconciling X" log line
with the request name and return — gives the smoke test something
to grep for. The kubebuilder TODO scaffolding is removed and replaced
with a Phase-pointing doc comment.
- Makefile manifests target now writes to ../charts/educates-installer/
{crds,templates/rbac}/ instead of bin/manifests/. Role name set to
"educates-installer-manager" to match the chart's hand-written
ClusterRoleBinding.
Verified: go build/vet/generate clean, helm lint passes (only the
benign "icon is recommended" info), helm template renders all four
expected resources (ServiceAccount, ClusterRole, ClusterRoleBinding,
Deployment).
Phase 0 step 6 / final: replace the kubebuilder-scaffolded reconciler tests with Phase 0 CEL validation specs, wire envtest to load CRDs from the chart, add a local kind-based smoke test, and add a repo-root CI workflow. CRD validation tests (envtest, ginkgo): - EducatesClusterConfig (config group): three specs — valid Managed-mode CR named "cluster" is accepted; CR with name != "cluster" is rejected by the singleton CEL; spec.mode change on update is rejected by the mode-immutability CEL. - Platform group: one shared file, one Describe per CRD. Each verifies singleton-name acceptance and rejection. - Both suite_test.go files now point at installer/charts/educates-installer/crds/ instead of the no-longer- present kubebuilder default config/crd/bases. - The kubebuilder-scaffolded reconciler tests (one per kind, with TODO placeholders and incompatible "test-resource" naming) are removed. Smoke test (hack/smoke-test.sh, local-only): - Creates kind cluster on demand, builds the operator image with make docker-build, kind-loads it, helm-installs the educates-installer chart, applies a minimal EducatesClusterConfig, and asserts the "Reconciling EducatesClusterConfig" log line appears within 60s. Tears down on exit unless KEEP_CLUSTER=true. - Wired into the previously-stubbed make smoke-test target. CI (.github/workflows/installer-operator-ci.yaml): - Triggers on changes to installer/operator/, the chart, go.work/go.work.sum, or the workflow itself. - Steps: go vet, go build, manifests-drift check, generate-drift check, make test (envtest), make lint. - Uses go-version-file pointing at the operator's go.mod so CI tracks whatever the project declares. Go-version pin lowered: - go.work and operator go.mod were both bumped to 1.25.7 by kubebuilder init. That triggered a "compile: version go1.25.7 does not match go tool version go1.25.6" warning chain under bash -e, which cascaded through the test recipe even though tests themselves passed. Lowered both to 1.25.0 — works under any 1.25.x toolchain and keeps the workspace consistent with the existing client-programs and node-ca- injector modules. Operator README: - Replaces the kubebuilder TODO-stub README with a tight summary of layout, the architecture docs, and the make targets.
Phase 1 step 1: extend the EducatesClusterConfig API surface with the two structural rules deferred from Phase 0 and the inter-CR contract fields component reconcilers will read. CEL exclusivity (two new rules on EducatesClusterConfigSpec): - When mode is Inline, the Managed-mode top-level fields (infrastructure, ingress, dns, policyEnforcement, imageRegistry) are forbidden. - When mode is Managed, spec.inline is forbidden. Combined with the existing mode-immutability rule, the spec now carries three CEL invariants. All three are envtest-verified. Status surface (the inter-CR contract): - status.mode echoes spec.mode at the time of last successful reconcile so components can branch without reading spec. - status.ingress (StatusIngress): domain, ingressClassName, wildcardCertificateSecretRef (NamespacedSecretRef — namespace + name), optional caCertificateSecretRef, optional clusterIssuerRef. - status.policyEnforcement (StatusPolicyEnforcement): clusterPolicyEngine, workshopPolicyEngine. - status.imageRegistry (reuses spec ImageRegistry shape): prefix and pullSecrets, populated even when empty so components see a single source of truth. Status fields are populated by the Phase 1 reconciler in the next step. The Managed-mode-only fields (bundledChartVersions) and conditions (InfrastructureConfigured, IngressReady, CertificatesReady, DNSReady, PolicyEnforcementReady) remain deferred to Phase 2/3 alongside their producing reconcilers. go build, go vet, make generate/manifests, and make test all pass; the generated CRD reflects all three CEL rules.
Phase 1 step 2: thread the operator's own namespace from the chart
through to the reconciler, and restrict the Secret cache to that
namespace.
- Chart Deployment template: inject OPERATOR_NAMESPACE via the
downward API (fieldRef metadata.namespace).
- cmd/main.go: read OPERATOR_NAMESPACE at startup; fail fast if unset
so a misconfigured Deployment doesn't silently misbehave.
- Manager cache.Options.ByObject restricts &corev1.Secret{} reads to
the operator namespace only — user-supplied Secrets referenced from
spec.inline live there, and the operator has no need to cache
Secrets cluster-wide. ClusterIssuers (cluster-scoped) and
IngressClasses (cluster-scoped) keep cluster-wide cache, as they
must.
- EducatesClusterConfigReconciler gains an OperatorNamespace string
field; main.go threads the value in. The Phase 0 stub doesn't read
it yet, but the wiring is now in place for the Inline validator
(steps 4-5).
go build, go vet, make test all pass; helm lint clean; helm template
shows the new env var injection in the rendered Deployment.
Phase 1 step 3: extend the operator's ClusterRole with read-only access to the resources Inline-mode validation needs to look up. - Secrets (core): for the wildcard TLS Secret, optional CA Secret, and imageRegistry pullSecrets. Reads are cache-restricted to the operator namespace by the Phase 1 step 2 cache.Options. - ClusterIssuers (cert-manager.io): when spec.inline.ingress. clusterIssuerRef is set, the validator checks existence and the Ready condition. - IngressClasses (networking.k8s.io): the validator checks the referenced IngressClass exists. All three are get/list/watch only — Inline mode never modifies cluster state. Markers added on the EducatesClusterConfigReconciler so controller-gen produces them; role.yaml regenerated into the chart.
Phase 1 step 4: implement the EducatesClusterConfig Inline-mode
reconciler. The operator now drains validates referenced cluster state
and publishes the inter-CR status contract that Phase 4 components
will consume.
Validator (validator.go):
- checkIngressClass: cluster-scoped Get against networkingv1.IngressClass.
- checkWildcardSecret: Get in operator namespace; assert tls.crt and
tls.key keys present.
- checkCASecret: optional; Get in operator namespace; assert ca.crt key
present.
- checkClusterIssuer: optional; unstructured.Unstructured against
cert-manager.io/v1 ClusterIssuer; assert status.conditions[Ready]==True.
IsNoMatchError (cert-manager CRD absent) is surfaced as a validation
error rather than a reconcile retry — matches how a user would
experience it. Vendored cert-manager Go types are deferred to Phase 2
per the recorded decision.
- validationError type carries spec.<field> path + reason so condition
messages name the offending input.
Reconcile flow (educatesclusterconfig_controller.go):
- Add finalizer "educatesclusterconfig.config.educates.dev/finalizer"
on first sight; first pass returns Requeue so the next pass sees a
stable resource version. Phase 1 deletion handler is a no-op; Phase
2 Managed-mode will uninstall charts here in reverse install order.
- For Inline mode: run validator → on success, populate
status.{mode,ingress,policyEnforcement,imageRegistry} and set
Ready=True / ValidationSucceeded=True. On failure: set Phase Degraded
and both conditions to False with the field-specific message.
status.imageRegistry is always populated (empty struct when unset)
so components see a single source of truth.
- For Managed mode: no-op stub until Phase 2.
- Defensive guard: if mode==Inline and spec.inline is nil (CEL bypass),
Degraded with "spec.inline required".
Envtest specs (validator_test.go):
- All-refs-valid → Ready, finalizer set, full status contract populated.
- Wildcard Secret missing → Degraded, message names the field +
"not found".
- Wildcard Secret present without tls.crt → Degraded.
- IngressClass missing → Degraded.
- Optional CA Secret referenced but missing → Degraded.
- Delete clears the finalizer and the apiserver removes the object.
Test helpers: makeWildcardSecret uses Opaque type because
kubernetes.io/tls Secrets are apiserver-validated to require both
tls.crt + tls.key, which would block the missing-key test.
go build / vet / test all pass; config-package coverage 63.2% (the
gap is mostly the unreachable Managed-mode branch in Reconcile, which
becomes covered once Phase 2 implements it).
…onalKyvernoPolicies The bundled Pod Security Standards (baseline + restricted) policies are now `ValidatingPolicy` resources, which compile to native ValidatingAdmissionPolicies and ignore Kyverno's resourceFilters ConfigMap the way the legacy ClusterPolicy path relied on. As a result they matched every Pod in the cluster and reported spurious audit violations against system namespaces such as kube-system and local-path-storage. The kyverno-cluster-policies.yaml template now injects a `spec.matchConstraints.namespaceSelector` into each bundled policy at render time, matching the `training.educates.dev/policy.engine` and `policy.name` labels session-manager stamps on its namespaces — baseline policies select [baseline, restricted], restricted policies select [restricted] — restoring the v3 ClusterPolicy scoping. The vendored policy files stay pristine; scoping is purely a template concern. Also widen `additionalKyvernoPolicies` support in the chart schemas: clusterSecurity (applied verbatim) now accepts a legacy ClusterPolicy or any policies.kyverno.io type (Validating/Mutating/Generating/Deleting); workshopSecurity (runtime-scoped per environment) accepts ClusterPolicy or ValidatingPolicy, matching what the runtime honours today. Scenarios 06 and 09 now exercise both a ClusterPolicy and a ValidatingPolicy on each path, with post-deploy assertions updated for the ValidatingPolicy resource kinds and the new namespace scoping. Repackages the embedded session-manager subchart tarball to carry these chart changes.
The cert-manager chart bump to v1.20.3 updated embed.go, SHA256SUMS, and the README but left load_test.go pinned to the removed v1.20.2 tarball, failing ci-operator. The version is hardcoded here (an import cycle stops the helm package test from referencing vendoredcharts.CertManagerVersion).
…ocol The four-place consistency test does not cover tests that hardcode a version string, so they go stale silently and only surface in CI. Add a straggler grep step, document the known cert-manager load_test.go case, and add a matching post-upgrade checklist item.
…r is deleted
The Inline-mode reconcile returned ctrl.Result{} on its terminal Ready
and validation-Degraded paths, so status was only re-evaluated on watch
events. The referenced ClusterIssuer is served by a deferred unstructured
informer (CRDWatcher); its delete event can be missed or observed against
a momentarily-stale cache, wedging status at Ready until the next spec
change. This was the watches_test.go ClusterIssuer-delete CI flake.
Add a belt-and-suspenders RequeueAfter to the Inline terminal returns so
the reconcile periodically re-runs validateInline and status self-heals,
matching the pattern already used by the Managed-mode cluster-service
reconcilers.
…orter The `educates local cluster create` flow previously mixed four output voices on two streams: hand-rolled `→` headers in the command, the registry package's plain sentence prints, the deploy reporter's `→`/`✓`/`·` lines, and kind's own logger. The reporter also corrupted lines on a TTY — it padded carriage-return rewrites by byte length (while `→`/`✓`/emoji are multi-byte) and left no clear-to-EOL, so interleaved writers produced garble like `registry mirror rror ghcr.io`. Harden the reporter: rewrites now erase to end of line (`\033[K`) and a step occupies a single line that morphs `→ …` into its `✓`/`✗` and commits with a newline. No width math, no trailing-space residue, multibyte-safe. Route the registry package through the reporter via a narrow nil-safe `Progress` (Update-only) interface; the command owns each step's lifecycle and the registry surfaces sub-operation detail. The docker image-pull stream is now drained silently instead of dumped to stdout where it corrupted the progress line. Reshape the deploy phase into an install narrative: the cached-secrets sync is a real step, each component collapses its apply+wait into one `Installing <Component>` line (the LookupService/SessionManager pair keeps apply-both-then-wait-both), the operator chart install reads `Installing Educates Kubernetes Operator`, and the closing line is `Educates successfully deployed`. Kind's own logger output is left untouched for now (a follow-up tames it).
kind's cluster bootstrap previously printed its own status spinner plus a
salutation/usage footer ("You can now use your cluster with…", "Have a
question…? 🙂"), which clashed with the rest of the create output and
couldn't be themed.
Give kind a custom logger (phaseLogger) instead of its CLI logger. kind's
status helper only drives its spinner for its own *cli.Logger, so for any
other logger it falls back to V(0).Infof per phase; phaseLogger captures
those, strips the marker/ellipsis, and forwards each phase to a callback.
The create command wires that callback to its progress step's Update, so
kind's multi-line spinner collapses onto one morphing line that finalizes
as "✓ creating kind cluster 'educates': ready". The footer is turned off
(DisplayUsage/DisplaySalutation false) and the "Cluster config used is
saved to…" print is gated behind --verbose.
With --verbose, kind's real logger (full spinner + detail on stderr) is
used and framed by a plain header/footer, since a morphing line can't
wrap kind's own terminal output. kind warnings/errors always reach
stderr.
Under --verbose the deploy/delete pipelines stream helm SDK debug to the same writer the progress reporter draws on, and kind uses its own terminal spinner. A morphing reporter (in-place \r rewrites) would be corrupted by that interleaved detail — the same failure class the reporter hardening fixed. Build the reporter with in-place morphing disabled when verbose (`isStdoutTTY(w) && !verbose`) in the create, deploy and delete paths, so every step state change commits its own line and interleaves cleanly with helm/kind detail. Default (non-verbose) keeps the clean one-line-per-step morphing. This also lets the kind step drop its special-cased verbose branch: a single runStep now handles both modes — default forwards kind's phases onto the morphing line, verbose lets kind print its full spinner while the non-morphing step frames it with committed header/footer lines.
The standalone `educates local registry deploy` and `local mirror deploy` commands rendered a single collapsed progress line with no way to see the underlying steps. Add a --verbose flag to both, threaded through stepOnStdout, which now builds its reporter with in-place morphing disabled when verbose so each sub-operation (pulling the image, adding registry config to the kind nodes, linking to the cluster network) is committed as its own line. Default output is unchanged. The other stepOnStdout callers (registry/mirror/cluster delete, prune) pass false.
…for air-gap The Contour ingress controller deployed inside a vcluster session used hardcoded upstream Contour/Envoy image refs that bypassed image relocation and were absent from the published image list, so vcluster ingress could not work in air-gapped or mirrored-registry environments. Make them first-class imageVersions entries (vcluster-internal-contour, vcluster-internal-envoy) and rewrite the loaded Contour manifests to the inventory refs at session creation, so they relocate via per-name overrides like the loft-sh images and are captured in the air-gap image list. Drop the dead contour-bundle inventory entry and its unused constant. Repackage the vendored session-manager subchart tarball. Also extend hack/generate-image-list.sh to render the session-manager chart so the full imageVersions inventory (workshop environments plus the vcluster application images) is included in the published list, and add publish-workshop-images to the image-list job's needs so the JDK/conda image digests resolve.
Add a None value to EducatesClusterConfig certificates.provider for installs that serve no in-cluster TLS, and a new ingress.protocol field (http or https) published as status.ingress.protocol so components learn the public URL scheme independently of whether the cluster terminates TLS. status.ingress.wildcardCertificateSecretRef becomes optional, and the inline ingress wildcard reference is now optional with its own protocol field. The cert-manager phase short-circuits for the None provider, marking CertificatesReady with reason CertificatesNotManaged and installing nothing. The platform reconcilers nil-guard the wildcard reference and pass the resolved protocol through to the runtime charts. A CEL rule restricts protocol http to the None provider. Regenerates the CRDs, the CLI-embedded chart copy, and the CRD-derived EducatesConfig schema.
…mination EducatesLocalConfig gains ingress.insecure, which translates to the None certificates provider with ingress.protocol http and skips the CA lookup and prerequisite. A local configuration with no ingress.domain now defaults to a <host-IP>.nip.io address with insecure enabled, since that address cannot obtain a trusted certificate. The nip.io and insecure defaulting is consolidated into the shared maybeApplyHostDomain helper used by render, deploy and cluster create. externalTLSTermination on the GKE, EKS and inline kinds now maps to the None provider with ingress.protocol https, so no unused in-cluster certificate is issued when TLS is terminated outside the cluster. The acme settings (GKE and EKS) and wildcardCertificateSecret (inline) become optional when it is set. A v3 clusterIngress.protocol http migrates to ingress.insecure.
… --defaults' local config view now prints the effective configuration with all CLI defaults materialised, so the values the CLI supplies (including the nip.io domain and the insecure default it implies) are explicit. A --raw flag prints the configuration file as written instead. When applying the defaults changes nothing beyond the file, the explanatory comment is skipped so the output matches --raw. local config init gains --defaults, which writes the fully-defaulted configuration into the file instead of the minimal apiVersion and kind. The yaml-language-server modeline is present on every output.
Add release-notes entries for the None certificates provider, ingress.insecure, the externalTLSTermination behaviour change, and the local config view and init updates. Correct the migration guide and the secure HTTP connections guide, which previously stated certificate-less installs were not possible, and describe the insecure local default in the local environment guide. Rework the quick start guide to lead with the plain HTTP default and add an optional section for serving workshops over HTTPS using a custom domain, the local DNS resolver and a local CA, with cross-reference labels for the relevant local environment sections.
The imagePrePuller toggle now defaults to true in EducatesLocalConfig, EducatesGKEConfig, EducatesEKSConfig and EducatesInlineConfig, so installs created from the CLI pre-pull workshop images onto every node ahead of time unless imagePrePuller is set to false. Updates WithDefaults, the embedded JSON schemas and the affected tests. The operator SessionManager CRD and the chart keep their disabled default, so installs applied directly from custom resources or the Helm chart are unaffected; the CLI supplies this opinionated default itself.
A secure local install (ingress.insecure not set) needs a cached CA for its domain, but the lookup only happened during deploy, after the kind cluster and registry were already created. A failed lookup then left a half-built cluster to delete by hand. Move the CA lookup into the cluster-create preflight, ahead of any Docker or cluster mutation, via a shared localCASecretIfSecure helper that deploy and create both use (an insecure install skips it). The lookup is a pure local-cache read, so a missing CA is reported without touching Docker at all. Also reword the missing-CA error: it now explains that a secure install is the default and offers both fixes, adding a CA or setting ingress.insecure to true, and shows the self-signed generate path as the simplest option.
set unmarshalled the file into a map, mutated it and re-marshalled, which dropped every comment (the yaml-language-server modeline included) and re-sorted the keys alphabetically, scrambling the file on each edit. Edit the YAML as a yaml.v3 node tree instead, so the file's comments and key order survive the round-trip. Intermediate mapping nodes are created as needed and existing nodes are updated in place. Output uses two-space indentation to match the files the CLI writes.
Add a schema-driven 'educates local config explain' command that describes configuration fields in the style of 'kubectl explain', from the embedded JSON schemas and without needing a cluster or a configuration file. A generic walker resolves $ref against $defs, descends into array items, merges allOf/anyOf/oneOf branch fields, and renders the field type, default, enum and description plus its sub-fields. The argument is a dotted field path; the kind is chosen with --kind (default EducatesLocalConfig), accepting a kind name or a short alias (local, gke, eks, inline, escape), so every config kind including the CRD-derived escape hatch can be explored. Also backfill field descriptions into the four scenario schemas, which were almost undocumented. The same descriptions drive editor hovers via the published schema, so the command and editors agree.
Document 'educates local config explain' in the configuration settings reference and the local environment guide, and add release-notes entries for the command and for the configuration schemas now carrying field documentation for every kind.
The deploy pipeline applied LookupService and SessionManager together but waited on them sequentially, so the user saw LookupService finish before SessionManager's line even appeared — even though the operator reconciles both at once. Add a concurrent multi-line progress group: on a TTY the two installs render as a contiguous block repainted in place (docker-pull style), each line morphing with its own status.phase, so one can go Ready while the other is still reconciling. On a non-TTY each state change appends a grep-able line. The two waits now run in parallel goroutines, each reporting to its own line. Apply (and display) SessionManager before LookupService. The apply order between the two is not load-bearing — SessionManager's reconciler watches LookupService and re-evaluates its Auto remote-access decision when one appears — so SessionManager goes first to read top-to-bottom in the block.
…inds none Previously 'educates local cluster create' errored with MissingLocalConfigError when <data-home>/config.yaml was absent, forcing a separate 'local config init' first. It now writes the minimal default EducatesLocalConfig in place, reports where it landed and how to view/edit it, and continues with defaults. The v3->v4 migration still takes priority: a laptop-kind values.yaml is migrated, and a non-laptop values.yaml still errors rather than being overwritten. The minimal-config template moves to the config package so 'local config init' and the auto-init stay byte-for-byte identical. Getting-started guides drop the mandatory init step and document the auto-create behaviour.
Make the `educates` CLI commands consistent across every command group. - Externalize usage examples into package-level vars and add an example to every leaf command that lacked one, so all commands document themselves consistently in `--help`. - Validate positional arguments in the cobra Args function via new utils.CmdError / CmdErrorFullUsage helpers, so a missing, extra, or invalid argument reports a clear message plus a `--help` pointer instead of cobra's generic "accepts 1 arg(s), received 0". Shared exactArgs/maximumArgs validators live in pkg/cmd/args.go. - Move the duplicated local secrets cache plumbing into pkg/secrets (CacheDir, WriteCachedSecret, LoadCachedSecret, ListCachedSecretNames, RemoveCachedSecret, ValidateSecretName, the secret builders, and the self-signed CA generator), and split `local secrets add` into one file per subcommand (ca, tls, docker-registry, generic). Covers the local, admin, cluster, docker, workshop, template, project, and tunnel groups.
… educates containers Add two commands, reimplemented against this branch (the referenced PR educates#871 is a divergent refactor on a different architecture and is not directly portable): - 'educates local mirror list' lists the deployed local registry mirrors as a table (name, URL, username, status, container name). - 'educates cluster portal export' exports a training portal and the workshops it references as sanitised, re-appliable Kubernetes YAML — to stdout as one stream, or with --as-files one file per resource. The --image-repository / --workshop-version options substitute the $(image_repository) / $(workshop_version) variables in the workshops. Give every container Educates creates via the Docker client a consistent set of educates.dev/* labels so they can be discovered, filtered, and cleaned up by label rather than by container name: - New pkg/constants holds the educates.dev/{app,role,mirror,url,username} keys and the registry/mirror/resolver role values. - The registry and mirror containers move to the namespaced keys (mirrors now also carry url/username); the resolver container, which previously had no labels, is labelled app=educates role=resolver. - ListRegistryMirrors and the bulk-delete path share one label filter. The kind node container is created by the kind library and keeps its own io.x-k8s.kind.* labels; it stays discovered by its well-known name.
Replace the per-command text/tabwriter blocks with the shared table helpers so tabular output is produced one way across the CLI. Column headers and output are unchanged. - utils.PrintTable: cluster portal list, cluster portal password --admin, cluster workshop list, cluster session list, docker workshop list. - utils.PrintKeyValuesTable (new): the cluster session status / extend / terminate detail output, previously a duplicated six-line block, now a shared printSessionDetails helper.
…cker preflight
'educates local cluster create' gains a --kubernetes-version flag (supported
1.33-1.36, default 1.36; an explicit --kind-cluster-image still wins), backed
by a new pkg/constants/kubernetes.go map of kind v0.32.0 node images.
The local Kind cluster can now be multi-node: EducatesLocalConfig cluster.nodes
declares each node's role, Kubernetes labels, and taints, rendered into the kind
config. A single control-plane remains the default when nodes is unset. Adds the
config type, the JSON schema entry, and a testdata fixture.
Adds a Docker-daemon preflight (docker.CheckDaemonRunning) run before every
command group that needs Docker (local cluster/registry/mirror/resolver and
docker workshops), so a stopped daemon reports a clear, actionable message
instead of a cryptic kind ("docker ps ... exit status 1") or Docker SDK error.
The port-availability preflight now pulls busybox only when it isn't already
cached locally.
Updates the educates-upgrade-kubernetes skill to the current supported set and
default, and notes to keep version-referencing code comments in sync on updates.
'educates local cluster status' now also reads the Educates platform CRs (EducatesClusterConfig, SecretsManager, LookupService, SessionManager) and reports whether Educates is installed and its components are ready. This distinguishes a cluster-only cluster (kind running, Educates not installed) from an installed-but-not-ready cluster from a fully installed and ready one. LookupService is optional; the other three are required, so an absent required component counts as not-ready. An unreachable API (e.g. a stopped cluster) is reported as unknown rather than failing the command.
…ling Reconcile the operator's own CRDs to the schemas embedded in the binary before the manager starts, so an imperative `helm upgrade` (which never touches the chart's crds/) and a declarative GitOps sync (which re-applies them) converge on identical CRD schemas at the moment a schema changes. `make manifests` now mirrors the generated CRDs into internal/crds/files/, .dockerignore re-includes them for the image build, and the operator gains customresourcedefinitions write access in its ClusterRole. Also harden and tidy the Helm and cluster-service layer: - Set explicit timeouts on all four Helm actions (Install/Upgrade/ Uninstall/Rollback) instead of inheriting the SDK's implicit 30s default, which is too tight for the slower hooks the vendored charts ship (Kyverno pre-delete cleanup, post-upgrade migration). - Add a decWait path (ActionWaitingUninstall): a release found mid-uninstall now requeues quietly instead of failing an install with "name still in use" and surfacing it as a reconcile error. - Make vendored-chart references in comments version-agnostic so they do not go stale on the next chart bump; correct the cert-manager crds note; drop process narrative from the cluster-admin binding doc. Sync the CLI-embedded copy of the installer chart.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work on migrating Carvel installer to Helm installer. Also, incorporating some of the changes in PR #871
educates local config editthat does validation and prevents saving on erroreducates local mirror listcommand--kubernetes-versionflag toeducates create-clusterand defaults to 1.36 and defines the kind images by sha to use.educates docker workshop deploy. It creates port bindings for additional services containers.cluster portal exportcommand to export a training portal and related workshops to stdout or files. Fixes new export-portal CLI command #91This has been rebased on latest develop, and updated to supersede #855 and #871. More commits will come later but guaranteeing that we keep this aligned with develop.