# syntax=docker/dockerfile:1

# Node 22 LTS on Alpine. The tag is deliberately unpinned: rebuilding this image is
# how you pick up new Node patches, Alpine packages and tool releases.
FROM node:22-alpine

# Alpine package versions are intentionally unpinned. This image tracks "current" —
# reproducibility comes from the published image tag, not from version pins that
# would rot against Alpine's rolling repositories.
# hadolint ignore=DL3018
RUN apk add --no-cache \
        bash \
        ca-certificates openssl \
        coreutils findutils diffutils gawk grep sed \
        file less patch tree \
        curl wget \
        jq yamllint \
        bind-tools iputils-ping netcat-openbsd socat rsync \
        git git-lfs openssh-client-default \
        ripgrep fd \
        tar gzip xz zip unzip \
        helm kubectl \
        make procps-ng su-exec tini tzdata

# Use bash with pipefail for the remaining build steps so a failure anywhere in a
# pipeline fails the layer.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# yq is not packaged for Alpine, so take the current upstream release. Running the
# binary immediately is the verification that matters here: it catches a truncated
# or HTML-error-page download, which is the realistic failure mode.
RUN arch="$(apk --print-arch | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" \
    && curl -fsSL -o /usr/local/bin/yq \
        "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" \
    && chmod 0755 /usr/local/bin/yq \
    && yq --version

# kubeconform validates manifests against the Kubernetes and CRD JSON schemas. Not
# packaged for Alpine either, so same treatment as yq: current upstream release, run it
# in the same layer so a truncated download fails the build.
#
# ALL schemas are VENDORED, which is the whole point of this layer. The yamllint MCP
# server in 50W/kube-manifests runs this image with NO network egress.
#
# Note that kubeconform's `-schema-location default` is a REMOTE URL
# (raw.githubusercontent.com), so it is useless here — with no egress it fails for
# EVERY kind, including built-ins like ConfigMap, and reports a download error rather
# than anything about the manifest. Both schema sets below are therefore local, and
# consumers must NOT pass `default`:
#   builtin/  yannh/kubernetes-json-schema, master-standalone-strict (~62 MB / 1505)
#   crds/     datreeio/CRDs-catalog, only the groups this cluster uses (~18 MB / 229)
#
# The CRD half matters most: without a local HelmRelease schema, a corrupted Flux
# HelmRelease — the exact thing this is here to catch — comes back "skipped" rather
# than "invalid". The full datree catalog is ~211 MB / 4177 schemas; sparse checkout
# means the other 4000 blobs are never downloaded. To cover a new group, add it to
# $groups and rebuild.
#
# Groups with NO published schema anywhere (toolhive.stacklok.dev, kagent.dev,
# openclaw.rocks, inference.llmkube.dev) are absent by necessity; kubeconform is
# invoked with -ignore-missing-schemas so those resources report as "skipped".
#
# Set BEFORE the RUN below so the build-time verification uses the same value consumers
# do. Used by the yamllint MCP shim in 50W/kube-manifests (toolhive/cm-yamllint-mcp.yaml).
ENV KUBECONFORM_SCHEMAS=/usr/local/share/kubeconform

# $groups is word-split on purpose — it is a list of directory names, and both
# `sparse-checkout set` and the `for` loop want it expanded into separate arguments.
# hadolint ignore=SC2086
RUN groups="acid.zalan.do ceph.rook.io cert-manager.io trust.cert-manager.io \
        cilium.io external-secrets.io gateway.networking.k8s.io \
        grafana.integreatly.org helm.toolkit.fluxcd.io jetstream.nats.io \
        k8s.keycloak.org k8up.io kustomize.config.k8s.io \
        kustomize.toolkit.fluxcd.io metallb.io monitoring.coreos.com \
        monitoring.grafana.com networking.istio.io objectbucket.io \
        operator.victoriametrics.com security.istio.io \
        source.toolkit.fluxcd.io telemetry.istio.io valkey.io" \
    && arch="$(apk --print-arch | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" \
    && curl -fsSL \
        "https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-${arch}.tar.gz" \
        | tar -xz -C /usr/local/bin kubeconform \
    && chmod 0755 /usr/local/bin/kubeconform \
    && kubeconform -v \
    && mkdir -p "${KUBECONFORM_SCHEMAS}/crds" "${KUBECONFORM_SCHEMAS}/builtin" \
    && git clone --depth 1 --filter=blob:none --sparse \
        https://github.com/datreeio/CRDs-catalog.git /tmp/crds \
    && git -C /tmp/crds sparse-checkout set $groups \
    && for g in $groups; do cp -a "/tmp/crds/$g" "${KUBECONFORM_SCHEMAS}/crds/"; done \
    && rm -rf /tmp/crds \
    && git clone --depth 1 --filter=blob:none --sparse \
        https://github.com/yannh/kubernetes-json-schema.git /tmp/kjs \
    && git -C /tmp/kjs sparse-checkout set master-standalone-strict \
    && cp -a /tmp/kjs/master-standalone-strict "${KUBECONFORM_SCHEMAS}/builtin/" \
    && rm -rf /tmp/kjs \
    # Prove the vendored schemas actually resolve, for a CRD kind AND a built-in kind.
    # `-schema-location default` is deliberately absent: it is a remote URL, so
    # including it would let this check pass on the network the build happens to have
    # and still fail in the egress-less pod. A missing or renamed schema directory
    # would otherwise only surface at run time as a silent "skipped", which is exactly
    # the failure this layer exists to prevent.
    && { printf 'apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n  name: t\nspec:\n  interval: 1h\n  chart:\n    spec:\n      chart: c\n      sourceRef:\n        kind: HelmRepository\n        name: r\n'; \
         printf -- '---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: c\n'; } \
        | kubeconform -strict -summary -verbose \
            -schema-location "${KUBECONFORM_SCHEMAS}/builtin/master-standalone-strict/{{.ResourceKind}}{{.KindSuffix}}.json" \
            -schema-location "${KUBECONFORM_SCHEMAS}/crds/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json" \
            -

# Defaults that keep non-interactive agent shells from hanging or failing:
#   * pagers off      — git/kubectl/helm otherwise block on `less` with no TTY
#   * no git prompts  — a private remote without creds fails fast instead of waiting
#   * helm homes /tmp — lets the image run with a read-only root filesystem
ENV PAGER=cat \
    GIT_PAGER=cat \
    GIT_TERMINAL_PROMPT=0 \
    HELM_CACHE_HOME=/tmp/.cache/helm \
    HELM_CONFIG_HOME=/tmp/.config/helm \
    HELM_DATA_HOME=/tmp/.local/share/helm

# tini reaps the grandchildren a shell workload leaves behind; the base image's
# docker-entrypoint.sh is kept so `node`-style invocations still work.
ENTRYPOINT ["/sbin/tini", "--", "docker-entrypoint.sh"]
CMD ["bash"]

# Last, so bumping build metadata does not invalidate the layers above.
ARG VERSION=dev
ARG REVISION=unknown
ARG CREATED=unknown
LABEL org.opencontainers.image.title="node-agent" \
      org.opencontainers.image.description="Node 22 LTS on Alpine with a GNU shell, network, and Kubernetes toolchain for AI agent command execution" \
      org.opencontainers.image.source="https://gitea.libretechconsulting.com/rmcguire/images" \
      org.opencontainers.image.base.name="docker.io/library/node:22-alpine" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.revision="${REVISION}" \
      org.opencontainers.image.created="${CREATED}"
