Skip to content

fix(helm): omit empty repository segment in bundled image path#2120

Merged
EItanya merged 4 commits into
kagent-dev:mainfrom
michaelspinks:fix/image-empty-repository-double-slash
Jul 2, 2026
Merged

fix(helm): omit empty repository segment in bundled image path#2120
EItanya merged 4 commits into
kagent-dev:mainfrom
michaelspinks:fix/image-empty-repository-double-slash

Conversation

@michaelspinks

Copy link
Copy Markdown
Contributor

What

Fix the bundled-PostgreSQL image helper so an empty image.repository renders a valid reference (registry/name:tag) instead of registry//name:tag, which is an invalid image reference and sends the pod into ImagePullBackOff. I observed this on AKS with Helm Chart 0.9.9.

Why

templates/_helpers.tpl builds the image with a printf that always emits the same registry/repository/name:tag shape — it writes a / between every segment whether or not that segment is empty:

{{- define "kagent.postgresql.image" -}}
{{- $pg := .Values.database.postgres.bundled -}}
{{- printf "%s/%s/%s:%s" $pg.image.registry $pg.image.repository $pg.image.name $pg.image.tag -}}
{{- end -}}

printf "%s/%s/%s:%s" always inserts a / between segments, so an empty repository (a valid configuration for images that live at the root of a registry) leaves an empty path segment and renders a double slash:

docker.io//postgres:18.3-alpine   # invalid reference -> ImagePullBackOff

The kagent.postgresql.image helper is unchanged across v0.9.9, v0.9.10, v0.10.0-beta1, and main (the printf line is identical at each), so the issue affects the current stable line and the next (v0.10.x) release line.

Fix

Build the path as a list, drop empty segments with compact, then join with /. This needs no if/else: an empty registry and/or repository is simply left out, so leaving repository blank produces a valid reference (the supported way to pull an image from the root of a registry) instead of a broken one:

{{- define "kagent.postgresql.image" -}}
{{- $pg := .Values.database.postgres.bundled -}}
{{- $parts := compact (list $pg.image.registry $pg.image.repository $pg.image.name) -}}
{{- printf "%s:%s" (join "/" $parts) $pg.image.tag -}}
{{- end -}}

Rendered results:

registry repository name result
docker.io library postgres docker.io/library/postgres:tag (unchanged)
docker.io "" postgres docker.io/postgres:tag (fixed)
"" "" postgres postgres:tag

Tests

Added one case to helm/kagent/tests/postgresql_test.yaml covering the empty-repository path (developed test-first). The existing default pgvector image and custom image tests already guard the non-empty path, so no regression test was
duplicated.

  - it: should omit empty repository segment in image
    template: postgresql.yaml
    documentIndex: 2
    set:
      database:
        postgres:
          bundled:
            image:
              registry: docker.io
              repository: ""
              name: postgres
              tag: "18.3-alpine"
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: docker.io/postgres:18.3-alpine
      - notMatchRegex:
          path: spec.template.spec.containers[0].image
          pattern: "//"

The test fails without this change (docker.io//postgres:18.3-alpine) and passes with it — revert the _helpers.tpl hunk to reproduce.

How to verify

make helm-version                                   # generate Chart.yaml from template
helm unittest helm/kagent                           # full suite green (incl. new case)
helm lint helm/kagent

# test-independent reproduction:
helm template t helm/kagent \
  --set database.postgres.bundled.image.repository="" | grep image:
# before: docker.io//postgres:18.3-alpine
# after:  docker.io/postgres:18.3-alpine

Scope

Limited to the bundled-PostgreSQL helper (kagent.postgresql.image), where the // template bug is confirmed. The doc2vec/QueryDoc MCP image lives in a separate chart (helm/tools/querydoc) and is assembled controller-side from IMAGE_REGISTRY / IMAGE_REPOSITORY / IMAGE_TAG env vars, so it is intentionally not changed here. If it exhibits the same // symptom at deploy time, it should be addressed separately (likely in the controller join logic), not in this chart.

A backport to the v0.9.x stable line may be worthwhile since the bug is present there — maintainers' call.

Checklist

  • Commit is DCO signed-off (git commit -s).
  • helm unittest helm/kagent green; helm lint helm/kagent clean.
  • Diff limited to templates/_helpers.tpl and tests/postgresql_test.yaml
    (generated Chart.yaml/Chart.lock/charts/*.tgz/dist/ excluded).
  • Test fails without the fix, passes with it.

Copilot AI review requested due to automatic review settings June 30, 2026 22:06
@github-actions github-actions Bot added the bug Something isn't working label Jun 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes Helm chart rendering for the bundled PostgreSQL image when database.postgres.bundled.image.repository is empty, avoiding invalid Docker references with a double slash (registry//name:tag) that can lead to ImagePullBackOff.

Changes:

  • Update kagent.postgresql.image helper to build the image path from non-empty segments and join with /.
  • Add a Helm unittest case to cover the empty-repository scenario and ensure // is not present.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
helm/kagent/templates/_helpers.tpl Build image reference by compacting empty path segments before joining, preventing registry//name:tag.
helm/kagent/tests/postgresql_test.yaml Add regression test validating docker.io/postgres:... output when repository is empty.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread helm/kagent/templates/_helpers.tpl
Signed-off-by: Mike Spinks <mikespinks@gmail.com>
@michaelspinks michaelspinks force-pushed the fix/image-empty-repository-double-slash branch from c1ddfef to fb7add8 Compare June 30, 2026 22:18
EItanya pushed a commit that referenced this pull request Jul 1, 2026
## What

Make the grafana-mcp chart render a valid image reference when
`image.registry` is left empty, so the registry can be omitted (e.g. to
pull from a mirror or let the runtime resolve the default registry)
without producing a leading-slash, unpullable reference.

## Why

The image is built inline in `templates/deployment.yaml`:

```gotemplate
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
```

The `/` between registry and repository is written unconditionally. If
`image.registry` is set to `""`, the result has a leading slash and is
not a valid image reference, so the pod enters `ImagePullBackOff`:

```
/grafana:latest     # invalid reference
```

There is currently no way to omit the registry and let the container
runtime resolve the repository against its default registry / a
configured mirror.

Omitting the registry is a configuration users legitimately need:

- **Air-gapped / mirrored environments.** Registry mirrors are
configured at the container runtime, which rewrites the host — so chart
values should carry only `repository:tag`.
- **Registry rewriting.** Admission controllers (e.g. Kyverno) prepend
an internal registry, so users leave `registry: ""`.
- **Consistency.** Other charts let you omit the registry; grafana-mcp
silently breaks with `ImagePullBackOff`, which reads like a bug.

> Note: the default `registry: mcp` is intentional and unchanged —
`mcp/grafana` resolves to `docker.io/mcp/grafana` (Docker Hub's `mcp`
namespace). This PR only makes an *empty* registry valid; the default
path still renders `mcp/grafana:latest`.
> This is the same 2-segment `registry/repository:tag` leading-slash
shape as the querydoc chart (#2124), and distinct from the
bundled-PostgreSQL `registry//name:tag` double-slash bug (#2120), which
uses a 3-segment shape. All fixed independently.

## Fix

Introduce a `grafana-mcp.image` helper (the chart currently has none)
that builds the path from non-empty segments via `compact` + `join`, and
call it from the deployment:

```gotemplate
{{- define "grafana-mcp.image" -}}
{{- $img := .Values.image -}}
{{- $parts := compact (list $img.registry $img.repository) -}}
{{- printf "%s:%s" (join "/" $parts) $img.tag -}}
{{- end -}}
```

```gotemplate
# templates/deployment.yaml
image: "{{ include "grafana-mcp.image" . }}"
```

Rendered results:

| registry | repository | tag | result |
|---|---|---|---|
| `mcp` | `grafana` | `latest` | `mcp/grafana:latest` (unchanged) |
| `""` | `grafana` | `latest` | `grafana:latest` (fixed) |

## Tests

Added two cases to `helm/tools/grafana-mcp/tests/deployment_test.yaml`
(developed test-first): a default-image regression guard (the suite had
no image assertion), and the empty-registry path.

```yaml
  - it: should render the default container image
    template: deployment.yaml
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: mcp/grafana:latest

  - it: should omit empty registry segment in image
    template: deployment.yaml
    set:
      image:
        registry: ""
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: grafana:latest
      - notMatchRegex:
          path: spec.template.spec.containers[0].image
          pattern: "^/"
```

The empty-registry test fails without this change (`/grafana:latest`)
and passes with it — revert the `deployment.yaml`/`_helpers.tpl` hunks
to reproduce.

## How to verify

```bash
make helm-tools                                       # generate grafana-mcp Chart.yaml
helm unittest helm/tools/grafana-mcp                  # full suite green (incl. new cases)
helm lint helm/tools/grafana-mcp

# test-independent reproduction:
helm template t helm/tools/grafana-mcp --set image.registry="" | grep image:
# before: /grafana:latest
# after:  grafana:latest
```

## Scope

Limited to the grafana-mcp chart's image construction. Empty
`repository` is not made valid here — omitting it leaves no image to
pull. This PR only makes `image.registry` optional.

## Checklist

- [x] Commit is DCO signed-off (`git commit -s`).
- [x] `helm unittest helm/tools/grafana-mcp` green; `helm lint
helm/tools/grafana-mcp` clean.
- [x] Default image still renders `mcp/grafana:latest` (regression
guard).
- [x] Diff limited to `templates/_helpers.tpl`,
`templates/deployment.yaml`, and `tests/deployment_test.yaml` (generated
chart files excluded).
- [x] Test fails without the fix, passes with it.

Signed-off-by: Mike Spinks <mikespinks@gmail.com>
EItanya pushed a commit that referenced this pull request Jul 1, 2026
## What

Make the querydoc (doc2vec) chart render a valid image reference when
`image.registry` is left empty, so the registry can be omitted (e.g. to
pull from Docker Hub or a mirror) without producing a leading-slash,
unpullable reference.

## Why

The image is built inline in `templates/deployment.yaml`:

```gotemplate
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
```

The `/` between registry and repository is written unconditionally. If
`image.registry` is set to `""`, the result has a leading slash and is
not a valid image reference, so the pod enters `ImagePullBackOff`:

```
/kagent-dev/doc2vec/mcp:1.1.10     # invalid reference
```

There is currently no way to omit the registry and let the container
runtime resolve the repository against its default registry / a
configured mirror.

Omitting the registry is a configuration users legitimately need:

- **Docker Hub.** A bare `org/image` reference resolves to Docker Hub,
so users mirroring the image there want `kagent-dev/doc2vec/mcp:tag`,
not `ghcr.io/...`.
- **Air-gapped / mirrored environments.** Registry mirrors are
configured at the container runtime, which rewrites the host — so chart
values should carry only `repository:tag`.
- **Registry rewriting.** Admission controllers (e.g. Kyverno) prepend
an internal registry, so users leave `registry: ""`.
- **Consistency.** Other charts let you omit the registry; querydoc
silently breaks with `ImagePullBackOff`, which reads like a bug.

> Note: this is a different issue from the bundled-PostgreSQL
`registry//name:tag`
> double-slash bug (#2120). That helper uses a 3-segment
`registry/repository/name:tag`
> shape; querydoc uses a 2-segment `registry/repository:tag` shape with
no separate
> image name, so the failure here is a leading slash from an empty
registry, not a
> double slash from an empty repository. They are fixed independently.

## Fix

Introduce a `querydoc.image` helper (the chart currently has none) that
builds the path from non-empty segments via `compact` + `join`, and call
it from the deployment:

```gotemplate
{{- define "querydoc.image" -}}
{{- $img := .Values.image -}}
{{- $parts := compact (list $img.registry $img.repository) -}}
{{- printf "%s:%s" (join "/" $parts) ($img.tag | default .Chart.AppVersion) -}}
{{- end -}}
```

```gotemplate
# templates/deployment.yaml
image: "{{ include "querydoc.image" . }}"
```

Rendered results:

| registry | repository | tag | result |
|---|---|---|---|
| `ghcr.io` | `kagent-dev/doc2vec/mcp` | `1.1.10` |
`ghcr.io/kagent-dev/doc2vec/mcp:1.1.10` (unchanged) |
| `ghcr.io` | `kagent-dev/doc2vec/mcp` | `v2.0.0` |
`ghcr.io/kagent-dev/doc2vec/mcp:v2.0.0` (unchanged) |
| `""` | `kagent-dev/doc2vec/mcp` | `1.1.10` |
`kagent-dev/doc2vec/mcp:1.1.10` (fixed) |

## Tests

Added one case to `helm/tools/querydoc/tests/deployment_test.yaml` for
the empty-registry path (developed test-first). The existing "should
have correct container image" and "should use custom image tag when set"
tests already guard the default and custom-tag paths, so no regression
test was duplicated.

```yaml
  - it: should omit empty registry segment in image
    template: deployment.yaml
    set:
      image:
        registry: ""
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: kagent-dev/doc2vec/mcp:1.1.10
      - notMatchRegex:
          path: spec.template.spec.containers[0].image
          pattern: "^/"
```

The test fails without this change (`/kagent-dev/doc2vec/mcp:1.1.10`)
and passes with it — revert the `deployment.yaml`/`_helpers.tpl` hunks
to reproduce.

## How to verify

```bash
make helm-tools                                     # generate querydoc Chart.yaml
helm unittest helm/tools/querydoc                   # full suite green (incl. new case)
helm lint helm/tools/querydoc

# test-independent reproduction:
helm template t helm/tools/querydoc --set image.registry="" | grep image:
# before: /kagent-dev/doc2vec/mcp:1.1.10
# after:  kagent-dev/doc2vec/mcp:1.1.10
```

## Scope

Limited to the querydoc chart's image construction. Empty `repository`
is not made valid here, because the repository carries the full image
path and there is no separate image-name segment — omitting it leaves no
image to pull. This PR only makes
`image.registry` optional.

## Checklist

- [x] Commit is DCO signed-off (`git commit -s`).
- [x] `helm unittest helm/tools/querydoc` green; `helm lint
helm/tools/querydoc` clean.
- [x] Diff limited to `templates/_helpers.tpl`,
`templates/deployment.yaml`, and
      `tests/deployment_test.yaml` (generated chart files excluded).
- [x] Test fails without the fix, passes with it.

Signed-off-by: Mike Spinks <mikespinks@gmail.com>
@EItanya EItanya merged commit 6a241b7 into kagent-dev:main Jul 2, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants