Skip to content

[Feature] Adding CE installer - #313

Open
royischoss wants to merge 17 commits into
mlrun:developmentfrom
royischoss:feature/ce-installer
Open

[Feature] Adding CE installer#313
royischoss wants to merge 17 commits into
mlrun:developmentfrom
royischoss:feature/ce-installer

Conversation

@royischoss

@royischoss royischoss commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

📝 Description

Adds the MLRun CE installer to this repo as scripts/install.sh — a single-file bash wrapper around helm install mlrun-ce/mlrun-ce with an optional ce-config.yaml config layer, pre-install validators, and a bats test suite. It ships alongside the chart it installs.

Chart behaviour is unchanged. The installer installs the published chart by default; this repo's chart is used only when the caller passes --chart-path ./charts/mlrun-ce. That is deliberate — the script is also served via curl | bash, where no repo exists around it, so the same invocation means the same thing in both places.

Implements CEML-731 (parent feature), CEML-732 (integrate the installer into the CE repo) and CEML-733 (installer CLI: pre-flight checks, ce-config.yaml schema, install flow).


🛠️ Changes Made

  • scripts/install.sh, scripts/ce-config.yaml.example, docs under scripts/docs/, and scripts/AGENTS.md (installer conventions and bug log)
  • tests/install_tests.bats — 98 unit tests, no cluster required
  • make installer-test / make installer-lint, and an Installer CI workflow running shellcheck + bats on every PR, plus a workflow_dispatch-only kind end-to-end install
  • Pre-install validators: blocking on Helm >= 3.6 (matching the chart README) and a default StorageClass; warning-only for Kubernetes version, registry auth, NodePort conflicts and node capacity
  • HELM_TIMEOUT (default 960s) — --wait previously inherited helm's 5m default and marked the release failed mid image pull
  • .gitignore for ce-config.yaml; installer linked from the root and chart READMEs; chart bumped to 0.12.0-rc.11

✅ Checklist

  • I have tested the changes in this PR
  • I confirmed whether my changes require a change in documentation and if so, I created another PR in MLRun for the relevant documentation.
  • I confirmed whether my changes require a changes in QA tests, for example: credentials changes, resources naming change and if so, I updated the relevant Jira ticket for QA.
  • I increased the Chart version in charts/mlrun-ce/Chart.yaml.
  • I confirmed that the installation works both on a local Docker Desktop environment and on a real cluster when using the required prerequisites.
    • If installation issues were found, I updated the relevant Jira ticket with the issue and steps to reproduce, or updated the prerequisites documentation if the issue is related to missing or outdated prerequisites.
  • If needed, update https://github.com/mlrun/ce/blob/development/charts/mlrun-ce/README.md with the relevant installation instructions and version Matrix.
  • If needed, update the following values files for multi namespace support:

No chart values changed, so the three install-mode values files need no update.


🧪 Testing

  • 98 bats unit tests (make installer-test), plus bash -n and shellcheck (make installer-lint) — all green
  • Dry-run against local Docker Desktop, both with --chart-path ./charts/mlrun-ce and with no chart flag (confirming the published chart is still the default)
  • Real install against an rke2 lab cluster via --kube-context through an SSH tunnel: 33/33 containers ready, helm statusdeployed

Docker Desktop was exercised via dry-run only, so the "both environments" checklist item is left unchecked: mlrun-ui has no linux/arm64 image and cannot come up on Apple Silicon.


🔗 References


🚨 Breaking Changes?

  • Yes (explain below)
  • No

🔍️ Additional Notes

Move the MLRun CE installer from its standalone repo into this one, so it
ships alongside the chart it installs.

The installer keeps installing the PUBLISHED chart by default. This repo's
chart is used only when the caller passes --chart-path ./charts/mlrun-ce.
The script is also served via curl | bash, where no repo exists around it,
so the same invocation has to mean the same thing in both places.

Repo integration:
- make installer-test / installer-lint targets (bats, bash -n, shellcheck)
- Installer CI workflow: lint and unit tests on PRs touching scripts/**,
  plus a workflow_dispatch-only kind end-to-end install
- gitignore ce-config.yaml so filled-in registry details can't be committed
- installer linked from the root and chart READMEs

Align the pre-install version validators with the chart's own prerequisites
rather than the product install docs: Helm >= 3.6 blocking, matching
charts/mlrun-ce/README.md, and no Kubernetes floor at all, since the chart
declares no kubeVersion and the README states no cluster version. The
previous K8s >= 1.34 / Helm >= 4.1 floors rejected nearly every supported
cluster and every Helm 3 user for a chart that renders fine on Helm 3.
MIN_K8S_VERSION now only warns; MIN_HELM_VERSION remains the one hard floor
and can be raised to tighten.
  workflow_dispatch-only kind end-to-end install
royischoss and others added 7 commits September 3, 2026 13:59
The resolve_external_host KUBE_CONTEXT test stubbed `command` but matched
on the subcommand immediately after shifting off "kubectl", so it never
saw the --context <ctx> that the kubectl wrapper injects ahead of the real
arguments. The stub returned nothing, node_ip came back empty and the
suggested host fell back to localhost.

It passed locally anyway: bats aborts a test on the first failed assertion
via set -e, and under macOS's system bash 3.2 that only holds for the last
statement in a @test. The broken assertion was second-to-last, so it was
swallowed and the test reported ok. CI runs bash 5, where it fails. Both
AGENTS.md and the run-tests skill now warn about this, since any test whose
stub drifts from the code can hide the same way.

Also bump the chart to 0.12.0-rc.12: development is already at rc.11, so
ct lint saw no version bump and failed.

Co-authored-by: Cursor <cursoragent@cursor.com>
"load_config exits 1 when yq is not installed" hid yq by setting
PATH=/usr/bin:/bin. That works on macOS, where yq lives in
/opt/homebrew/bin, but the GitHub runners ship yq in /usr/bin — so on CI
yq stayed on PATH, load_config parsed the file and returned 0, and the
test failed. install.sh's yq guard was correct all along; only the test's
isolation was wrong.

Replace the hardcoded PATH with an _empty_bin helper pointing at a
directory that provably holds no executables, so the assertion no longer
depends on where the host installs yq. The neighbouring "no yq required
when CONFIG_FILE is empty" test used the same hardcoded PATH and was
therefore vacuous on CI; it now uses the helper too.

Co-authored-by: Cursor <cursoragent@cursor.com>
It's a working design document rather than reference material for the
chart, so it stays on disk and out of the PR via .git/info/exclude
instead of the tracked .gitignore.

Strip the four AGENTS.md pointers and the install.sh NodePort comment
that referenced it, so nothing in the repo links to a file reviewers
won't have. The NodePort note now points at REQUIRED_NODEPORTS, which
is the actual source of truth for that list.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the Phase 1-6 status list and the Phase 3 pre-work notes: they record
how the installer was built rather than how it works, which belongs in the
local design doc, not the repo. Two genuinely reusable pieces are kept and
restated without the phase framing — the value precedence rule (flag > env >
config > --values > chart defaults, and why --config and -f compose) now sits
under the install flow it describes, and the version floors read as current
policy instead of a "realigned from X" note.

De-specify the bug entries: they cited a named internal lab cluster, and one
paragraph pinned live state on it, including an SSH command with an internal
hostname and node IP. The findings hold for any remote cluster reached via
--kube-context, so they now say that instead, and the teardown command is
given with a placeholder context.

Co-authored-by: Cursor <cursoragent@cursor.com>
The test hardcoded a named internal lab cluster and its node IP. Neither
means anything to a reader of this repo, and the address is a real private
one. Swap in a placeholder context name and 192.0.2.10, from RFC 5737's
documentation range, so the fixture is self-evidently fake.

Co-authored-by: Cursor <cursoragent@cursor.com>
The installer had no version at all, so a user couldn't say which script
they ran and a run couldn't be reproduced. Add --version/-v, reading the
version from charts/mlrun-ce/Chart.yaml beside the script rather than
storing a copy: bumping the chart bumps the installer, with nothing to
carry forward by hand. Run standalone (curl | bash, or copied to a bin
directory) there's no chart to read and nothing recording where the script
came from, so it reports unknown instead of inventing a number.

Tying the version to the chart rather than giving the installer its own is
deliberate. The script encodes chart internals — the fixed NodePort list,
and the --set value paths it writes — so an installer and a chart from the
same tag are the only pairing guaranteed to agree, and a renamed value path
would otherwise fail silently as a --set that does nothing.

The documented curl URLs pointed at .../development/scripts/install.sh,
which is a 404 today (scripts/ isn't on development yet) and would be a
moving target once it isn't. Use a pinned mlrun-ce-<version> tag as the
primary form, since chart-releaser already tags every release and those
tags contain this script; keep development documented as the rolling
alternative. No new release workflow is needed as a result.

Co-authored-by: Cursor <cursoragent@cursor.com>
@royischoss
royischoss marked this pull request as ready for review September 3, 2026 11:56
@royischoss
royischoss requested a lite review from Copilot September 3, 2026 11:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are a few confirmed functional issues (GitHub Actions branch filter glob, CoreDNS patch namespace lookup, and --enable-otel collector not disabling auto-instrumentation toggles) that should be fixed before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a first-class MLRun CE installer (scripts/install.sh) into the CE chart repository, alongside docs and a dedicated CI workflow, while keeping chart behavior unchanged (published chart is still the default install target unless --chart-path is used).

Changes:

  • Add scripts/install.sh (Bash installer) with config-file support (ce-config.yaml), preflight validators, and install/uninstall flows.
  • Add installer documentation (scripts/README.md, scripts/docs/*), example config, and internal conventions (scripts/AGENTS.md).
  • Add an installer unit test suite (tests/install_tests.bats), Makefile targets, and a new GitHub Actions workflow (Installer CI); bump chart version to 0.12.0-rc.12 and link the installer from existing READMEs.
File summaries
File Description
scripts/install.sh New single-file installer wrapping Helm install/upgrade, config loading, validators, and helpers (progress UI, notes parsing).
tests/install_tests.bats New bats-core unit test suite for installer behavior (parsing/config/validators).
scripts/README.md Installer top-level usage and flows (published chart vs local chart).
scripts/docs/{parameters,configuration,faq}.md Detailed reference docs for flags/env/config schema, precedence, and gotchas.
scripts/ce-config.yaml.example Example installer: config block for repeatable installs.
.github/workflows/installer-ci.yaml New CI workflow for shellcheck + bats; optional kind E2E via workflow_dispatch.
Makefile Add installer-test and installer-lint targets.
charts/mlrun-ce/Chart.yaml Bump chart version to 0.12.0-rc.12.
README.md, charts/mlrun-ce/README.md, CONTRIBUTING.md, AGENTS.md Link/document the installer and related tooling expectations.
.gitignore Ignore filled-in ce-config.yaml.
.claude/skills/* Update internal skills/docs to reflect installer/testing/versioning conventions.
Review details

Suppressed comments (1)

scripts/install.sh:270

  • patch_coredns_for_registry looks up the ingress-nginx-controller Service in ${NAMESPACE}, but ingress controllers are typically deployed in ingress-nginx (or another dedicated namespace). This makes the CoreDNS patch silently skip in the common case.
patch_coredns_for_registry() {
    local registry_host="$1"
    local ingress_clusterip
    ingress_clusterip="$(kubectl get svc ingress-nginx-controller \
        --namespace "${NAMESPACE}" \
        -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)"
  • Files reviewed: 17/18 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +10 to +14
pull_request:
branches:
- development
- "[0-9]+.[0-9]+.x"
workflow_dispatch:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this against the docs and I don't think it holds — GitHub's branch filters aren't plain globs. The filter pattern cheat sheet defines + as "matches one or more of the preceding character" and [] as "matches one alphanumeric character listed in the brackets or included in ranges", so [0-9]+.[0-9]+.x does match 0.12.x.

It's also the same filter ci.yaml already uses, so changing it here would make the two workflows disagree about which branches they run on. Leaving as is.

Comment thread scripts/install.sh
Comment on lines +235 to +240
# Docker installed
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed or not in PATH."
log_info "Install Docker: https://docs.docker.com/get-docker/"
exit 1
fi

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one, deliberately rather than by omission.

The helm/kubectl checks use type -P for a reason specific to them: the script defines its own kubectl and helm wrapper functions (to inject --context), so command -v would always find the script's own wrapper and the check would never fail. There is no docker wrapper, so that reasoning doesn't carry over.

Going the other way has a real cost. type -P docker would reject setups where docker is legitimately an exported shell function — a podman shim, or a wrapper pointing at a remote host — even though every docker call the installer makes would work fine. And the docker info check on the next line already catches anything genuinely broken, with a better error message than a presence check could give.

Comment thread scripts/install.sh
Comment thread scripts/install.sh
Comment thread scripts/install.sh
royischoss and others added 4 commits September 3, 2026 15:07
…sion

The modes were encoded as flags — --uninstall, and install as the unnamed
default — which reads as a script rather than a CLI. Add a verb in front:
install, uninstall, version, help, dispatched by parse_command so parse_args
stays a pure flag parser.

Nothing that worked before stops working. A leading flag, or no arguments at
all, still means install, so every documented invocation and the curl | bash
one-liner are unchanged, and `uninstall` and --uninstall are the same thing.
A bare word that isn't a known command is an error rather than an install:
`mlrun-ce-installer unistall` should not deploy a cluster on a typo.

Two details worth knowing. main() expands COMMAND_ARGS through the
${a[@]+"${a[@]}"} guard because bash < 4.4 — including the macOS system bash
this is developed on — treats an empty array as unset under set -u, so an
argument-less run would otherwise abort; there's a test pinning that case.
And the uninstall assignment is a full if rather than [[ ]] && x=y, which
would return 1 and take errexit with it whenever the command wasn't uninstall.

Also suppress color when stdout isn't a terminal or NO_COLOR is set. The log
helpers previously emitted escape bytes unconditionally, which landed in CI
logs and any redirected output.

The installed command is now mlrun-ce-installer rather than mlrun-install.

Co-authored-by: Cursor <cursoragent@cursor.com>
deploy_local_registry() had no DRY_RUN guard, so it ran kubectl apply
unconditionally. On a cluster without the namespace the apply failed and
errexit aborted the run, making --local-registry --dry-run unusable. On a
cluster where the namespace existed the apply succeeded, so a run advertised
as rendering-only really deployed a registry Deployment and Service and
reported success.

The guard returns early under DRY_RUN, placed after the LOCAL_REGISTRY_URL
assignment so the URL still reaches the rendered --set flags. Verified live:
the dry run renders it into nuclio's registry_url ConfigMap and mlrun's api
chief/worker deployments while creating nothing.

CI missed this because the kind-install job uses --local-registry for a real
install, never with --dry-run.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed functional/documentation issues in the installer around dry-run behavior, CoreDNS patch ingress service lookup, and password-file handling that can cause unintended cluster mutations or auth failures.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

Previously missed (4) — in code that hasn't changed since the last review.

scripts/install.sh:304

  • patch_coredns_for_registry looks up ingress-nginx-controller in the MLRun namespace ($NAMESPACE), but ingress controllers are typically deployed in their own namespace (e.g. ingress-nginx). This makes the CoreDNS patch skip even when the controller exists, reducing the usefulness of the --local-registry --enable-ingress flow.
    scripts/install.sh:452
  • Using echo to return an env-var value can mis-handle values that look like echo flags (e.g. -n) and is less reliable for returning raw strings; prefer printf for output from helpers.

This issue also appears on line 470 of the same file.
scripts/install.sh:633

  • Reading REGISTRY_PASSWORD_FILE via command substitution preserves trailing newlines (common at EOF), which can make the resulting docker-registry secret invalid and cause auth failures. Strip CR/LF when loading the password file.
    scripts/install.sh:1505
  • --dry-run is documented as not creating resources, but --local-registry currently still deploys a Deployment/Service (and may patch CoreDNS). Dry-run should skip local-registry deployment to avoid mutating the cluster.

scripts/install.sh:470

  • prompt_or_env returns the resolved value via echo, which can behave unexpectedly for certain values (e.g. leading -n, backslashes). Use printf to return the exact string.
    echo "${value:-$default}"
  • Files reviewed: 17/18 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread scripts/README.md Outdated
royischoss and others added 2 commits September 7, 2026 13:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants