diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fab49d0..110b65c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,6 +142,113 @@ jobs: name: e2e_coverage path: tests_coverage + e2e_buildkitd_driver_forwarding: + name: End-to-end tests (buildkitd driver forwarding) + runs-on: ubuntu-22.04 + timeout-minutes: 30 + # The mirror image of e2e_buildx_config, for a driver that cannot succeed + # without a cluster. The configuration names the kubernetes buildkitd driver + # and a namespace; TRDL_BUILDX_DRIVER only spells out the fallback the code + # would take anyway, so that the path this job forbids is written down. + # The release therefore has to FAIL, and with the driver's own error naming + # that namespace: if the driver stops being forwarded the build falls back to + # the docker CLI and succeeds, and if the OPTIONS stop being forwarded the + # namespace is the kubeconfig's instead. The kubeconfig below points at a + # closed port precisely so the driver gets past client configuration and + # reaches pod creation, which is where the namespace becomes observable. + env: + TRDL_BUILDX_DRIVER: docker-container + TRDL_TEST_BUILDKITD_DRIVER: kubernetes + TRDL_TEST_BUILDKITD_DRIVER_OPTS_NAMESPACE: namespace=trdl-forwarding-guard + KUBECONFIG: /tmp/guard-kubeconfig.yaml + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: e2e/go.mod + + - name: Install Task + uses: go-task/setup-task@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up prebuilt trdl test binary + run: | + task --yes client:build-with-coverage + echo TRDL_TEST_BINARY_PATH=$GITHUB_WORKSPACE/bin/coverage/trdl >> $GITHUB_ENV + echo TRDL_TEST_COVERAGE_DIR=$GITHUB_WORKSPACE/tests_coverage/e2e >> $GITHUB_ENV + + - name: Set up git config + run: task --yes ci:setup:git-config + + - name: Prepare environment + run: | + sudo apt-get update + sudo apt-get install -y gpg + task --yes server:deps:install:c + + - name: Install 3p-git-signatures + run: task --yes ci:install:3p-git-signatures + + - name: Install ginkgo + run: task --yes -p deps:install:ginkgo + + - name: Write a kubeconfig pointing at a closed port + run: | + # Its namespace is deliberately NOT the configured one: the driver must + # take the namespace from buildkitd_driver_opts, and this is what makes + # the difference observable in the error. + cat > /tmp/guard-kubeconfig.yaml <<'EOF' + apiVersion: v1 + kind: Config + clusters: + - name: unreachable + cluster: + server: https://127.0.0.1:1 + contexts: + - name: unreachable + context: + cluster: unreachable + user: none + namespace: kubeconfig-namespace + current-context: unreachable + users: + - name: none + user: {} + EOF + + - name: Setup vault + run: | + task --yes server:setup-vault-local + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Test + run: | + set -o pipefail + task --yes e2e:test:e2e paths='./tests/flow_vault' labelFilter='buildkitd-driver-forwarding' 2>&1 | tee /tmp/guard.log + + - name: Assert the guard actually ran + run: | + # A green ginkgo run proves nothing on its own: it exits 0 when the + # label filter selects no spec at all, and at --vv it prints a skipped + # spec's name too, so neither the exit code nor the spec's name shows + # that this guard executed. + # No anchor on the first: ginkgo colours that line, so it does not + # start with "Ran". + grep -qE 'Ran [1-9][0-9]* of [0-9]+ Specs' /tmp/guard.log + # Only the guard itself can put this in the log — it is the release + # output the spec asserts on, and no other spec produces it. + grep -q 'unable to create builder pod trdl-forwarding-guard/' /tmp/guard.log + + - name: Upload coverage artifact + uses: actions/upload-artifact@v7 + with: + name: e2e_coverage_buildkitd_driver_forwarding + path: tests_coverage + e2e_buildx_config: name: End-to-end tests (buildx driver from configure) runs-on: ubuntu-22.04 @@ -275,6 +382,7 @@ jobs: - e2e_tests - e2e_buildkit - e2e_buildx_config + - e2e_buildkitd_driver_forwarding runs-on: ubuntu-22.04 steps: - name: Checkout code @@ -299,6 +407,7 @@ jobs: - e2e_tests - e2e_buildkit - e2e_buildx_config + - e2e_buildkitd_driver_forwarding - upload_coverage uses: werf/common-ci/.github/workflows/notification.yml@main secrets: diff --git a/.github/workflows/tests_buildx_kubernetes_driver.yaml b/.github/workflows/tests_buildx_kubernetes_driver.yaml index c7d71739..42e8afed 100644 --- a/.github/workflows/tests_buildx_kubernetes_driver.yaml +++ b/.github/workflows/tests_buildx_kubernetes_driver.yaml @@ -1,15 +1,22 @@ -name: Tests. Buildx kubernetes driver +name: Tests. In-cluster Kubernetes builders -# Opt-in: the buildx kubernetes driver needs a cluster, so this is not part of -# the required checks. It runs the flow_vault suite — the real Vault plugin, -# a real release build and buildkit secret mounts — against in-cluster BuildKit -# pods instead of the default docker-container builder. +# Opt-in: both jobs need a cluster, so this is not part of the required checks. +# Each runs the flow_vault suite — the real Vault plugin, a real release build +# and buildkit secret mounts — against in-cluster BuildKit pods instead of the +# default docker-container builder: one through the buildx kubernetes driver and +# the docker CLI, one through the plugin's own kubernetes buildkitd driver. on: workflow_dispatch: +permissions: + contents: read + env: TASK_X_REMOTE_TASKFILES: 1 KIND_VERSION: v0.32.0 + # Pinned so a mutable release asset cannot become whatever it likes before it is + # installed as root; from the release's own kind-linux-amd64.sha256sum. + KIND_SHA256: 50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54 jobs: e2e_tests_buildx_kubernetes: @@ -22,6 +29,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v6 @@ -62,6 +71,7 @@ jobs: - name: Set up kind cluster run: | curl -fsSLo /tmp/kind https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64 + echo "${KIND_SHA256} /tmp/kind" | sha256sum --check --strict sudo install -m 0755 /tmp/kind /usr/local/bin/kind kind create cluster --name trdl-buildx kubectl cluster-info --context kind-trdl-buildx @@ -80,3 +90,97 @@ jobs: with: name: e2e_coverage_buildx_kubernetes path: tests_coverage + + e2e_tests_buildkitd_kubernetes: + name: End-to-end tests (kubernetes buildkitd driver) + runs-on: ubuntu-22.04 + timeout-minutes: 30 + env: + # The plugin provisions the builder itself, configured per project rather + # than through the environment. TRDL_BUILDX_DRIVER names a driver that + # cannot work, so the release passes only while the docker CLI path stays + # unused: a regression routing back to it fails on this value. + TRDL_BUILDX_DRIVER: this-driver-does-not-exist + TRDL_TEST_BUILDKITD_DRIVER: kubernetes + TRDL_TEST_BUILDKITD_DRIVER_OPTS_NAMESPACE: namespace=default + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: e2e/go.mod + + - name: Install Task + uses: go-task/setup-task@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up prebuilt trdl test binary + run: | + task --yes client:build-with-coverage + echo TRDL_TEST_BINARY_PATH=$GITHUB_WORKSPACE/bin/coverage/trdl >> $GITHUB_ENV + echo TRDL_TEST_COVERAGE_DIR=$GITHUB_WORKSPACE/tests_coverage/e2e >> $GITHUB_ENV + + - name: Set up git config + run: task --yes ci:setup:git-config + + - name: Prepare environment + run: | + sudo apt-get update + sudo apt-get install -y gpg + task --yes server:deps:install:c + + - name: Install 3p-git-signatures + run: task --yes ci:install:3p-git-signatures + + - name: Install ginkgo + run: task --yes -p deps:install:ginkgo + + - name: Setup vault + run: | + task --yes server:setup-vault-local + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Set up kind cluster + run: | + curl -fsSLo /tmp/kind https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64 + echo "${KIND_SHA256} /tmp/kind" | sha256sum --check --strict + sudo install -m 0755 /tmp/kind /usr/local/bin/kind + kind create cluster --name trdl-buildkitd + kubectl cluster-info --context kind-trdl-buildkitd + + - name: Test + # Only the complete-cycle spec: this job sets TRDL_TEST_BUILDKITD_DRIVER, + # which also registers the negative forwarding guard, and the suite's + # AfterEach removes the plugin binary BeforeSuite built once, so a second + # spec would find nothing to enable. + run: task --yes e2e:test:e2e paths='./tests/flow_vault' labelFilter='flow' + + - name: Collect builder diagnostics + if: failure() + run: kubectl get pods --all-namespaces || true + + - name: Assert no builder pod was left behind + if: always() + run: | + # An empty pod list only means "nothing left behind" if the query itself + # ran. Probe the API first, so an unreachable cluster fails this step + # instead of passing it silently. + kubectl get namespace default > /dev/null + kubectl get pods -n default --no-headers > /tmp/pods-after.txt + echo "pods still in the namespace: $(wc -l < /tmp/pods-after.txt)" + + if grep '^trdl-builder-' /tmp/pods-after.txt; then + echo "the release left a builder pod behind" + exit 1 + fi + + - name: Upload coverage artifact + uses: actions/upload-artifact@v7 + with: + name: e2e_coverage_buildkitd_kubernetes + path: tests_coverage diff --git a/docs/_includes/reference/vault_plugin/configure.md b/docs/_includes/reference/vault_plugin/configure.md index c332b538..6be7cff3 100644 --- a/docs/_includes/reference/vault_plugin/configure.md +++ b/docs/_includes/reference/vault_plugin/configure.md @@ -9,9 +9,11 @@ Configure the plugin. ### Parameters -* `buildkitd_address` (string, optional) — An address of a running buildkitd (unix://, tcp://, docker-container:// or kube-pod:// scheme) to build release artifacts with the BuildKit client; the docker CLI is used if not set. Build secrets are sent to that daemon, and tcp:// is neither encrypted nor authenticated, so securing the channel and isolating the daemon is the administrator's responsibility. -* `buildx_driver` (string, optional) — The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable, and cannot be combined with buildkitd_address. -* `buildx_driver_opts` (array, optional) — The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables, and cannot be combined with buildkitd_address. +* `buildkitd_address` (string, optional) — An address of a running buildkitd (unix://, tcp://, docker-container:// or kube-pod:// scheme) to build release artifacts with the BuildKit client; the docker CLI is used only when neither this nor buildkitd_driver is set. Build secrets are sent to that daemon, and tcp:// is neither encrypted nor authenticated, so securing the channel and isolating the daemon is the administrator's responsibility. +* `buildkitd_driver` (string, optional) — Provision an ephemeral buildkitd per build instead of using the docker CLI: kubernetes runs it as a pod and needs no docker binary next to the plugin. Cannot be combined with buildkitd_address, buildx_driver or buildx_driver_opts. A TRDL_BUILDKITD_ADDRESS set on the process wins over a stored driver, and the build reports the driver as unused. +* `buildkitd_driver_opts` (array, optional) — The buildkitd driver options, one name=value pair per element (e.g. namespace=trdl-build); they require buildkitd_driver to be set. The kubernetes driver accepts annotations, deadline, image, labels, limits.cpu, limits.ephemeral-storage, limits.memory, namespace, nodeselector, requests.cpu, requests.ephemeral-storage, requests.memory, rootless, serviceaccount and timeout; anything else is rejected. +* `buildx_driver` (string, optional) — The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable, and cannot be combined with buildkitd_address or buildkitd_driver. +* `buildx_driver_opts` (array, optional) — The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables, and cannot be combined with buildkitd_address or buildkitd_driver. * `git_repo_url` (string, required) — URL of the Git repository. * `git_trdl_channels_branch` (string, optional) — A special Git branch to store the trdl channels configuration file. * `git_trdl_channels_path` (string, optional) — A path in the Git repository to the trdl channels configuration file (trdl_channels.yaml is used by default). diff --git a/docs/pages_en/QUICKSTART.md b/docs/pages_en/QUICKSTART.md index 2f43cd82..4da4db1b 100644 --- a/docs/pages_en/QUICKSTART.md +++ b/docs/pages_en/QUICKSTART.md @@ -62,7 +62,7 @@ The same two settings are also available per project in the plugin configuration Each of the two settings is resolved on its own: the plugin configuration takes precedence over the environment, and the environment takes precedence over the default `docker-container` driver with no options. A field left out of `configure`, or set to an empty value, means "not configured" and falls back to the environment — it does not override it with an empty value. To build with no driver options at all while the environment defines some, unset those variables. -Neither field can be combined with `buildkitd_address` (see below): that setting replaces the buildx path entirely, no builder is created, and the driver settings would have no effect — so `configure` rejects the combination instead of ignoring them. The check covers one `configure` call only. A `TRDL_BUILDKITD_ADDRESS` set on the Vault process also wins over these settings, and since it is process-wide and can be changed after a project is configured, it cannot be rejected at that point: the build reports the settings as unused in the plugin log instead. +Neither field can be combined with `buildkitd_address` or `buildkitd_driver` (see below): those settings replace the buildx path entirely, no builder is created through the docker CLI, and the driver settings would have no effect — so `configure` rejects the combination instead of ignoring them. The check covers one `configure` call only. A `TRDL_BUILDKITD_ADDRESS` set on the Vault process also wins over these settings, and since it is process-wide and can be changed after a project is configured, it cannot be rejected at that point: the build reports the settings as unused in the plugin log instead. Notes on the `kubernetes` driver: @@ -71,9 +71,67 @@ Notes on the `kubernetes` driver: * rootless BuildKit (`rootless=true`) does not fit the `baseline` PodSecurity level either: buildx gives the builder pod `seccompProfile: Unconfined` and the `unconfined` AppArmor annotation, and both are already forbidden at `baseline`. The builder namespace has to be labelled `privileged` (or be exempt from PodSecurity admission); * see the [buildx kubernetes driver documentation](https://docs.docker.com/build/builders/drivers/kubernetes/) for the available driver options. -#### Building against an external buildkitd +#### Building without the buildx drivers -Both buildx drivers above shell out to the `docker` CLI, so they require the binary to be present next to the plugin. When the plugin runs in an environment without the `docker` binary (for example, embedded into another process shipped in a distroless image), the build can be pointed at an already running `buildkitd` instead: the plugin then talks to it directly with the BuildKit client, and no builder is provisioned or removed per build. +Both buildx drivers above shell out to the `docker` CLI, so they require the binary to be present next to the plugin. Two settings replace that path, and both talk to BuildKit with the Go client: + +* `buildkitd_driver` — the plugin provisions an ephemeral `buildkitd` itself, one per build, and removes it afterwards. It executes no external binary at all, which is what makes it usable where no `docker` exists — for example when the plugin is embedded into another process shipped in a distroless image; +* `buildkitd_address` — the plugin connects to a `buildkitd` somebody else runs, and provisions nothing. Whether it needs a binary depends on the scheme: `unix://` and `tcp://` do not, while `docker-container://` and `kube-pod://` still shell out to `docker` and `kubectl` respectively (see below). + +Only one of them can be set, and `configure` refuses either of them next to the buildx *fields*. The buildx *environment* variables are a different matter: `TRDL_BUILDX_DRIVER` and `TRDL_BUILDX_DRIVER_OPTS_*` are a fallback for the fields, and a configured `buildkitd_driver` or `buildkitd_address` simply wins over them, because a process-wide variable cannot be rejected when a project is configured. + +##### Provisioning an ephemeral buildkitd + +`buildkitd_driver=kubernetes` runs the builder as a Pod for the duration of one build: + +```shell +vault write trdl-test-project/configure ... \ + buildkitd_driver=kubernetes \ + buildkitd_driver_opts=namespace=trdl-build \ + buildkitd_driver_opts=serviceaccount=trdl-buildkit +``` + +or, as a JSON payload: + +```json +{ + "buildkitd_driver": "kubernetes", + "buildkitd_driver_opts": ["namespace=trdl-build", "serviceaccount=trdl-buildkit"] +} +``` + +The options are `name=value` pairs, one per list element and passed through as is, so a value containing commas such as `nodeselector=disktype=ssd,zone=a` needs no escaping. They are validated when the configuration is written — an option the driver cannot honor is rejected there rather than by a release that fails later. The kubernetes driver accepts: + +| Option | Meaning | +|---|---| +| `namespace` | the namespace to run the builder in; defaults to the namespace named by the current kubeconfig context, then to the namespace of the plugin's own ServiceAccount, and to `default` when neither names one | +| `image` | the buildkitd image; defaults to `moby/buildkit:buildx-stable-1`, or its `-rootless` variant when `rootless=true` | +| `rootless` | run rootless BuildKit | +| `serviceaccount` | the ServiceAccount for the builder pod | +| `nodeselector`, `labels`, `annotations` | comma-separated `name=value` pairs | +| `requests.cpu`, `requests.memory`, `requests.ephemeral-storage` | pod resource requests | +| `limits.cpu`, `limits.memory`, `limits.ephemeral-storage` | pod resource limits | +| `timeout` | how long to wait for the builder to become ready, e.g. `5m`; `2m` by default | +| `deadline` | a hard lifetime cap for the builder pod (`activeDeadlineSeconds`), e.g. `2h`; unset by default, and a whole number of seconds of at least `1s`. It is not a grace period: a build still running when it expires is killed too, so set it above the longest release this project takes | + +The option names are the buildx kubernetes driver's own wherever the two overlap, but the vocabulary is this driver's, not buildx's: options buildx accepts and this driver does not — `replicas`, `loadbalance`, `tolerations`, `schedulername`, `qemu.*`, the persistent-volume options — are rejected, and `deadline` has no buildx counterpart. + +What the plugin needs in the target namespace is `create`, `get` and `delete` on `pods`, plus `create` on `pods/exec`; a namespaced Role is enough. No `apps` API group is used. The build stream rides the API server's exec channel, so the plugin needs no network route to the builder pod. The cluster is targeted via the standard kubeconfig or in-cluster ServiceAccount resolution. + +Notes on the pod: + +* the namespace must exist and must admit the builder pod. By default the container runs `privileged`; with `rootless=true` it runs unprivileged but needs seccomp `Unconfined` and the `unconfined` AppArmor annotation instead. Either way the `baseline` PodSecurity level forbids it, so the namespace has to be labelled `privileged` or be exempt from PodSecurity admission — the same requirement the buildx `kubernetes` driver has. Unlike the buildx path, the rejection arrives directly from the `create` call rather than as a readiness timeout; +* the pod is removed when the build ends, including when it fails or is canceled, and when the builder never becomes ready. It is not removed if the plugin's own process dies outright, and not if the delete itself fails — a lost API connection, a withdrawn `delete` permission. That failure is reported in the release log and the plugin log, but it does not fail the release, so a privileged pod can outlive a build that reported success. `deadline` is what bounds both cases, at the cost of also capping a legitimate build; +* the builder is a bare Pod with `restartPolicy: Never`, deliberately: nothing may replace it mid-build, because the replacement would be a builder the release is not connected to. + +Two things follow from the plugin creating the pod with its own credentials, and both are the administrator's to weigh: + +* **`configure` write access becomes pod-create access.** Whoever can write a project's configuration chooses the `namespace` the builder runs in, the `serviceaccount` it runs as and the `image` it runs — anywhere the plugin's own Role reaches. Restrict `/configure` to the same people who are trusted with the release keys, and keep the plugin's Role to namespaces that hold nothing else worth taking. Give the builder a ServiceAccount whose privileges you have audited in full: creating a workload lets it run as any ServiceAccount of that namespace, and a ServiceAccount carries whatever is bound to it directly, through a namespaced RoleBinding, through a ClusterRoleBinding, and through group bindings such as `system:serviceaccounts`. The absence of a RoleBinding in the namespace is not by itself an isolation guarantee, and the builder pod does mount its token. +* **The builder container is privileged by default.** BuildKit needs it; `rootless=true` trades it for seccomp `Unconfined` and the `unconfined` AppArmor annotation. Either way the build executes project-supplied instructions in a container the `baseline` PodSecurity level would refuse, so give it a namespace and nodes you are willing to lose, not the ones the signing keys live on. + +##### Connecting to an existing buildkitd + +The build can instead be pointed at an already running `buildkitd`: the plugin then talks to it directly with the BuildKit client, and no builder is provisioned or removed per build. The buildkitd address is set per project in the plugin configuration: @@ -86,7 +144,7 @@ or, as a fallback for all projects, with the `TRDL_BUILDKITD_ADDRESS` environmen * `unix://` and `tcp://` — direct gRPC connection to buildkitd, no external binaries required; * `docker-container://` and `kube-pod://` — connection through `docker exec`/`kubectl exec`, requiring the corresponding CLI. -When `buildkitd_address` is not set, builds go through `docker buildx` exactly as described above. Deploying `buildkitd` itself is out of trdl's scope: on Kubernetes it is typically a Deployment or StatefulSet in a dedicated namespace whose PodSecurity labels are managed by the cluster owner, since BuildKit requires a relaxed seccomp/AppArmor profile even in rootless mode. +When neither `buildkitd_address` nor `buildkitd_driver` is set, builds go through `docker buildx` exactly as described above. Deploying `buildkitd` itself is out of trdl's scope: on Kubernetes it is typically a Deployment or StatefulSet in a dedicated namespace whose PodSecurity labels are managed by the cluster owner, since BuildKit requires a relaxed seccomp/AppArmor profile even in rootless mode. Securing the connection and isolating the daemon is the administrator's responsibility. The plugin sends the entire build context and every build secret over this connection — the project build secrets and, when mac signing is configured, the signing certificate, its password and the notary key. What that requires: diff --git a/docs/pages_ru/QUICKSTART.md b/docs/pages_ru/QUICKSTART.md index 66653fb0..7152e912 100644 --- a/docs/pages_ru/QUICKSTART.md +++ b/docs/pages_ru/QUICKSTART.md @@ -61,7 +61,7 @@ TRDL_BUILDX_DRIVER_OPTS_KUBE='namespace=trdl-build;rootless=true' Каждая из двух настроек разрешается независимо: конфигурация плагина имеет приоритет над переменными окружения, а переменные окружения — над умолчанием, то есть драйвером `docker-container` без опций. Поле, не переданное в `configure` или переданное пустым, означает «не задано» и отдаёт решение переменным окружения, а не перекрывает их пустым значением. Чтобы собирать вообще без опций драйвера, когда в окружении они заданы, эти переменные нужно снять. -Ни одно из этих полей нельзя задать вместе с `buildkitd_address` (см. ниже): та настройка заменяет весь buildx-путь, сборщик не создаётся, и настройки драйвера ни на что не влияли бы — поэтому `configure` отвергает такую комбинацию, а не игнорирует их. Проверка касается только одного вызова `configure`. Переменная `TRDL_BUILDKITD_ADDRESS`, заданная процессу Vault, тоже имеет приоритет над этими настройками, но она процессная и может измениться уже после настройки проекта, поэтому отвергнуть её на этом этапе нельзя: вместо этого сборка сообщает в лог плагина, что настройки не используются. +Ни одно из этих полей нельзя задать вместе с `buildkitd_address` или `buildkitd_driver` (см. ниже): эти настройки заменяют весь buildx-путь, сборщик через CLI `docker` не создаётся, и настройки драйвера ни на что не влияли бы — поэтому `configure` отвергает такую комбинацию, а не игнорирует их. Проверка касается только одного вызова `configure`. Переменная `TRDL_BUILDKITD_ADDRESS`, заданная процессу Vault, тоже имеет приоритет над этими настройками, но она процессная и может измениться уже после настройки проекта, поэтому отвергнуть её на этом этапе нельзя: вместо этого сборка сообщает в лог плагина, что настройки не используются. Особенности драйвера `kubernetes`: @@ -70,9 +70,67 @@ TRDL_BUILDX_DRIVER_OPTS_KUBE='namespace=trdl-build;rootless=true' * rootless BuildKit (`rootless=true`) не укладывается и в уровень PodSecurity `baseline`: buildx задаёт поду сборщика `seccompProfile: Unconfined` и AppArmor-аннотацию `unconfined`, а оба этих значения запрещены уже на `baseline`. Namespace сборщика должен быть помечен как `privileged` (либо исключён из PodSecurity-admission); * доступные опции драйвера — в [документации buildx kubernetes driver](https://docs.docker.com/build/builders/drivers/kubernetes/). -#### Сборка через внешний buildkitd +#### Сборка без buildx-драйверов -Оба buildx-драйвера выше запускают CLI `docker`, поэтому требуют наличия бинарника рядом с плагином. Если плагин работает в окружении без бинарника `docker` (например, встроен в другой процесс, поставляемый в distroless-образе), сборку можно направить на уже запущенный `buildkitd`: плагин обращается к нему напрямую через клиент BuildKit, и сборщик не создаётся и не удаляется на каждую сборку. +Оба buildx-драйвера выше запускают CLI `docker`, поэтому требуют наличия бинарника рядом с плагином. Этот путь заменяют две настройки, и обе обращаются к BuildKit через Go-клиент: + +* `buildkitd_driver` — плагин сам поднимает временный `buildkitd`, по одному на сборку, и удаляет его после неё. Внешних бинарников он не запускает вовсе, поэтому годится там, где `docker` отсутствует, — например когда плагин встроен в другой процесс, поставляемый в distroless-образе; +* `buildkitd_address` — плагин подключается к `buildkitd`, который запускает кто-то другой, и ничего не создаёт. Нужен ли бинарник, зависит от схемы: `unix://` и `tcp://` обходятся без него, а `docker-container://` и `kube-pod://` по-прежнему запускают `docker` и `kubectl` соответственно (см. ниже). + +Задать можно только один из них, и `configure` отвергает любой из них рядом с *полями* buildx. С *переменными окружения* buildx иначе: `TRDL_BUILDX_DRIVER` и `TRDL_BUILDX_DRIVER_OPTS_*` — это запасной вариант для полей, и заданный `buildkitd_driver` или `buildkitd_address` просто побеждает их, потому что процессную переменную нельзя отвергнуть в момент настройки проекта. + +##### Временный buildkitd, поднимаемый плагином + +`buildkitd_driver=kubernetes` запускает сборщик подом на время одной сборки: + +```shell +vault write trdl-test-project/configure ... \ + buildkitd_driver=kubernetes \ + buildkitd_driver_opts=namespace=trdl-build \ + buildkitd_driver_opts=serviceaccount=trdl-buildkit +``` + +или в виде JSON: + +```json +{ + "buildkitd_driver": "kubernetes", + "buildkitd_driver_opts": ["namespace=trdl-build", "serviceaccount=trdl-buildkit"] +} +``` + +Опции — пары `name=value`, по одной на элемент списка, передаются как есть, поэтому значение с запятыми вроде `nodeselector=disktype=ssd,zone=a` не нужно экранировать. Они проверяются в момент записи конфигурации: опцию, которую драйвер не умеет применять, отвергает `configure`, а не упавший позже релиз. Драйвер `kubernetes` принимает: + +| Опция | Значение | +|---|---| +| `namespace` | namespace для сборщика; по умолчанию — namespace текущего контекста kubeconfig, затем namespace собственного ServiceAccount плагина, а если не задан ни там ни там — `default` | +| `image` | образ buildkitd; по умолчанию `moby/buildkit:buildx-stable-1`, а при `rootless=true` — его вариант `-rootless` | +| `rootless` | запускать rootless BuildKit | +| `serviceaccount` | ServiceAccount для пода сборщика | +| `nodeselector`, `labels`, `annotations` | пары `name=value` через запятую | +| `requests.cpu`, `requests.memory`, `requests.ephemeral-storage` | requests пода | +| `limits.cpu`, `limits.memory`, `limits.ephemeral-storage` | limits пода | +| `timeout` | сколько ждать готовности сборщика, например `5m`; по умолчанию `2m` | +| `deadline` | жёсткий предел времени жизни пода сборщика (`activeDeadlineSeconds`), например `2h`; по умолчанию не задан, задаётся целым числом секунд не меньше `1s`. Это не льготный период: сборка, которая к этому моменту ещё идёт, тоже будет убита, поэтому значение берут заведомо больше самого долгого релиза проекта | + +Имена опций совпадают с опциями buildx-драйвера `kubernetes` там, где опции пересекаются, но набор здесь свой, а не buildx: опции, которые принимает buildx и не принимает этот драйвер — `replicas`, `loadbalance`, `tolerations`, `schedulername`, `qemu.*`, опции постоянного тома, — отвергаются, а у `deadline` соответствия в buildx нет вовсе. + +В целевом namespace плагину нужны `create`, `get` и `delete` на `pods` плюс `create` на `pods/exec`; namespaced-роли достаточно. Группа API `apps` не используется. Поток сборки идёт через exec-канал API-сервера, поэтому сетевой доступ от плагина к поду сборщика не нужен. Кластер определяется стандартным способом — через kubeconfig или in-cluster ServiceAccount. + +Особенности пода: + +* namespace должен существовать и должен пропускать под сборщика. По умолчанию контейнер запускается `privileged`; при `rootless=true` он непривилегированный, но требует seccomp `Unconfined` и AppArmor-аннотации `unconfined`. И то и другое запрещено на уровне PodSecurity `baseline`, поэтому namespace должен быть помечен как `privileged` либо исключён из PodSecurity-admission — ровно то же требование, что и у buildx-драйвера `kubernetes`. В отличие от buildx-пути, отказ приходит прямо из вызова `create`, а не в виде таймаута готовности; +* под удаляется по окончании сборки, в том числе при её падении и отмене, а также если сборщик так и не стал готов. Он не удаляется, если сам процесс плагина умер целиком, и если само удаление не прошло — оборвалась связь с API, отозвали право `delete`. Об этом сообщают лог релиза и лог плагина, но релиз при этом не падает, поэтому привилегированный под может пережить сборку, отчитавшуюся успехом. Оба случая ограничивает `deadline` — ценой такого же ограничения для нормальной сборки; +* сборщик — именно отдельный Pod с `restartPolicy: Never`, и это осознанно: его нельзя подменять посреди сборки, потому что замена окажется сборщиком, с которым релиз не связан. + +Из того, что под создаёт сам плагин своими кредами, следуют две вещи, и обе — предмет решения администратора: + +* **Право записи в `configure` становится правом создавать поды.** Кто может писать конфигурацию проекта, тот выбирает `namespace`, в котором поднимется сборщик, `serviceaccount`, под которым он побежит, и `image`, который в нём запустится, — везде, куда достаёт Role самого плагина. Ограничьте доступ на запись в `/configure` тем же кругом, которому доверены релизные ключи, а Role плагина — namespace'ами, в которых больше нечего брать. Сборщику давайте ServiceAccount, права которого проверены целиком: создание workload'а позволяет запуститься от имени любого ServiceAccount этого namespace, а ServiceAccount несёт всё, что привязано к нему напрямую, через namespaced RoleBinding, через ClusterRoleBinding и через групповые привязки вроде `system:serviceaccounts`. Отсутствие RoleBinding в namespace само по себе гарантией изоляции не является, и токен под сборщика монтируется. +* **Контейнер сборщика по умолчанию привилегированный.** Этого требует BuildKit; `rootless=true` меняет привилегию на seccomp `Unconfined` и AppArmor-аннотацию `unconfined`. В любом случае сборка исполняет инструкции проекта в контейнере, который уровень PodSecurity `baseline` не пропустил бы, — значит и namespace, и узлы под неё надо давать те, которые не жалко, а не те, где лежат ключи подписи. + +##### Подключение к существующему buildkitd + +Сборку можно направить на уже запущенный `buildkitd`: плагин обращается к нему напрямую через клиент BuildKit, и сборщик не создаётся и не удаляется на каждую сборку. Адрес buildkitd задаётся для каждого проекта в конфигурации плагина: @@ -85,7 +143,7 @@ vault write trdl-test-project/configure ... buildkitd_address=tcp://buildkitd.tr * `unix://` и `tcp://` — прямое gRPC-соединение с buildkitd, внешние бинарники не нужны; * `docker-container://` и `kube-pod://` — соединение через `docker exec`/`kubectl exec`, требуется соответствующий CLI. -Если `buildkitd_address` не задан, сборка идёт через `docker buildx`, как описано выше. Развёртывание самого `buildkitd` находится вне зоны ответственности trdl: в Kubernetes это обычно Deployment или StatefulSet в отдельном namespace, метки PodSecurity которого задаёт владелец кластера, поскольку BuildKit требует ослабленный профиль seccomp/AppArmor даже в rootless-режиме. +Если не заданы ни `buildkitd_address`, ни `buildkitd_driver`, сборка идёт через `docker buildx`, как описано выше. Развёртывание самого `buildkitd` находится вне зоны ответственности trdl: в Kubernetes это обычно Deployment или StatefulSet в отдельном namespace, метки PodSecurity которого задаёт владелец кластера, поскольку BuildKit требует ослабленный профиль seccomp/AppArmor даже в rootless-режиме. Защита канала и изоляция демона — ответственность администратора. Через это соединение плагин передаёт весь контекст сборки и все секреты сборки: секреты проекта, а если настроена подпись для macOS — сертификат подписи, его пароль и notary-ключ. Что из этого следует: diff --git a/e2e/go.mod b/e2e/go.mod index b93b8df2..cb235c65 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -40,6 +40,7 @@ require ( github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.3.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/djherbis/buffer v1.2.0 // indirect github.com/djherbis/nio/v3 v3.0.1 // indirect @@ -50,6 +51,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-git/go-git/v5 v5.19.1 // indirect @@ -58,6 +60,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -68,6 +71,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -93,6 +97,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jellydator/ttlcache/v3 v3.4.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -106,8 +111,13 @@ require ( github.com/moby/buildkit v0.31.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/moby/sys/signal v0.7.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -127,6 +137,7 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/tonistiigi/fsutil v0.0.0-20260716115106-30cd4fc5d911 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect @@ -134,6 +145,7 @@ require ( github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/werf/lockgate v0.1.1 // indirect github.com/werf/logboek v0.6.1 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -148,8 +160,10 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect @@ -160,9 +174,18 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) replace ( diff --git a/e2e/go.sum b/e2e/go.sum index 981bbacd..edc79101 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -106,6 +106,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= @@ -120,6 +122,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -141,6 +145,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -149,6 +181,8 @@ github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -160,6 +194,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -177,6 +213,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -246,9 +284,13 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -294,6 +336,8 @@ github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc h1:dvhPFj1niuMP3CBCjhiZWJQr//+w1LOA8cHclFJnNe0= github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc/go.mod h1:frGYJTxenVCGPa9doaqZSU9FqzT7bt+1dFeVAaCFoyQ= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= @@ -305,12 +349,20 @@ github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -399,6 +451,8 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -427,12 +481,16 @@ github.com/werf/lockgate v0.1.1 h1:S400JFYjtWfE4i4LY9FA8zx0fMdfui9DPrBiTciCrx4= github.com/werf/lockgate v0.1.1/go.mod h1:0yIFSLq9ausy6ejNxF5uUBf/Ib6daMAfXuCaTMZJzIE= github.com/werf/logboek v0.6.1 h1:oEe6FkmlKg0z0n80oZjLplj6sXcBeLleCkjfOOZEL2g= github.com/werf/logboek v0.6.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -461,34 +519,52 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -505,15 +581,22 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -529,6 +612,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -543,5 +630,23 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/e2e/tests/flow_vault/buildkitd_driver_forwarding_test.go b/e2e/tests/flow_vault/buildkitd_driver_forwarding_test.go new file mode 100644 index 00000000..fa4a65c9 --- /dev/null +++ b/e2e/tests/flow_vault/buildkitd_driver_forwarding_test.go @@ -0,0 +1,99 @@ +package flow + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// The two lines carrying buildkitd_driver and buildkitd_driver_opts from the +// stored configuration into the release build have nothing else covering them: +// delete them and every other suite stays green, because the build falls back to +// the docker CLI, which works on the runner. +// +// This guard needs no cluster. The configuration names the kubernetes driver, +// which cannot come up here, while the environment names a docker-container +// buildx driver that can. The release therefore has to fail, and it has to fail +// with the driver's own error: a build that succeeds means the configured driver +// never reached it. +// Registered only when the guard job asks for it, and NOT skipped at runtime: +// the suite's AfterEach runs `server:dev:cleanup`, whose first line deletes the +// plugin binary that BeforeSuite builds once for the whole suite. A spec that +// exists and skips still runs that AfterEach, which leaves the next spec unable +// to enable the plugin at all. So this suite tolerates exactly one live spec, +// and the label filter in the job is what keeps it to one. +// guardNamespace is the namespace the job passes through +// buildkitd_driver_opts. It must differ from the kubeconfig's namespace, or the +// assertion below would hold even with the options dropped. +const guardNamespace = "trdl-forwarding-guard" + +func init() { + if os.Getenv("TRDL_TEST_BUILDKITD_DRIVER") != "kubernetes" { + return + } + + Describe("kubernetes buildkitd driver forwarding", Label("e2e", "trdl", "buildkitd-driver-forwarding"), func() { + It("fails the release with the driver's own error instead of building through the docker CLI", func() { + projectName := "buildkitd-driver-forwarding" + // The same fixture identities the flow test uses; these are key + // fingerprints, not addresses. + pgpKeys := map[string]string{ + "developer": "74E1259029B147CB4033E8B80D4C9C140E8A1030", + "tl": "2BA55FD8158034EEBE92AA9ED9D79B63AFC30C7A", + "pm": "C353F279F552B3EF16DAE0A64354E51BF178F735", + } + + By("initializing git repo") + { + importGPGKeys(pgpKeys) + for _, v := range pgpKeys { + SuiteData.GPGKeys = append(SuiteData.GPGKeys, v) + } + initGitRepo(SuiteData.TestDir, "main") + } + + By("setup minio and vault") + { + setupMinio(projectName) + setupVault(SuiteData.TestDir) + } + + By("configure server") + { + serverInitProject(SuiteData.TestDir, projectName) + serverConfigureProject(SuiteData.TestDir, serverConfigureOptions{ + ProjectName: projectName, + RepoURL: SuiteData.TestDir, + TrdlChannelsBranch: "main", + RequiredNumberOfVerifiedSignaturesOnCommit: 3, + S3Endpoint: "http://localhost:9000", + S3Region: "ru-central1", + S3AccessKeyID: "minioadmin", + S3SecretAccessKey: "minioadmin", + S3BucketName: projectName, + }) + serverAddGPGKeys(SuiteData.TestDir, projectName, pgpKeys) + } + + By("releasing a signed tag") + { + gitTag(SuiteData.TestDir, "v1.0.0", pgpKeys["developer"]) + quorumSignTag(SuiteData.TestDir, pgpKeys["tl"], pgpKeys["pm"], "v1.0.0") + + output := serverReleaseExpectingFailure(SuiteData.TrdlVaultClientBinPath, projectName, "v1.0.0") + + // Two things have to be true, and the namespace is what makes the + // second one observable. The error has to come from the driver at + // all — the docker CLI path would have succeeded here — and it has + // to name the namespace the OPTIONS carried, not the one the + // kubeconfig names. Dropping buildkitd_driver_opts anywhere along + // the way leaves the kubeconfig's namespace in this message. + // "unable to create", not just "builder pod": the readiness log line + // carries the same namespace, so a bare match would also be satisfied + // by a pod that came up, should this job ever gain a cluster. + Expect(output).Should(ContainSubstring("unable to create builder pod " + guardNamespace + "/")) + } + }) + }) +} diff --git a/e2e/tests/flow_vault/utils.go b/e2e/tests/flow_vault/utils.go index 42dbb5e4..adc75b93 100644 --- a/e2e/tests/flow_vault/utils.go +++ b/e2e/tests/flow_vault/utils.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "runtime" + "sort" "strings" . "github.com/onsi/gomega" @@ -227,8 +228,35 @@ func serverConfigureProject(testDir string, opts serverConfigureOptions) { return "" }() - testutil.RunSucceedCommand( - testDir, + // Set TRDL_TEST_BUILDKITD_DRIVER to have the plugin provision the builder + // itself, with no docker binary involved. Its options come one per + // TRDL_TEST_BUILDKITD_DRIVER_OPTS_ variable, the way the plugin's own + // TRDL_BUILDX_DRIVER_OPTS_* work: a documented option value can contain + // commas, as `nodeselector=disktype=ssd,zone=a` does, so nothing may split + // them. + buildkitdDriver := func() string { + if driver := os.Getenv("TRDL_TEST_BUILDKITD_DRIVER"); driver != "" { + return fmt.Sprintf("buildkitd_driver=%s", driver) + } + return "" + }() + + var buildkitdDriverOpts []string + var driverOptNames []string + for _, keyValue := range os.Environ() { + if name, _, _ := strings.Cut(keyValue, "="); strings.HasPrefix(name, "TRDL_TEST_BUILDKITD_DRIVER_OPTS_") { + driverOptNames = append(driverOptNames, name) + } + } + sort.Strings(driverOptNames) + + for _, name := range driverOptNames { + if driverOpt := os.Getenv(name); driverOpt != "" { + buildkitdDriverOpts = append(buildkitdDriverOpts, fmt.Sprintf("buildkitd_driver_opts=%s", driverOpt)) + } + } + + args := []string{ "vault", "write", vaultAddress, fmt.Sprintf("%s/configure", opts.ProjectName), @@ -243,7 +271,11 @@ func serverConfigureProject(testDir string, opts serverConfigureOptions) { lastPubCommit, buildkitdAddress, buildxDriver, - ) + buildkitdDriver, + } + args = append(args, buildkitdDriverOpts...) + + testutil.RunSucceedCommand(testDir, args[0], args[1:]...) } func serverAddBuildSecrets(testDir, projectName string, secrets map[string]string) { @@ -286,6 +318,20 @@ func serverAddGPGKeys(testDir, projectName string, keys map[string]string) { } } +// serverReleaseExpectingFailure runs the same release and returns its output +// instead of asserting success, so a test can state which failure it expects. +func serverReleaseExpectingFailure(bin, projectName, tagName string) string { + output, err := testutil.RunCommandWithOptions( + "", + bin, + []string{"release", projectName, tagName, "--token", "root", "--max-attempts", "1"}, + testutil.RunCommandOptions{ShouldSucceed: false}, + ) + Expect(err).Should(HaveOccurred(), "the release was expected to fail") + + return string(output) +} + func serverRelease(bin, projectName, tagName string) { testutil.RunSucceedCommand( "", diff --git a/server/go.mod b/server/go.mod index e1f952d0..174649ee 100644 --- a/server/go.mod +++ b/server/go.mod @@ -21,7 +21,7 @@ require ( github.com/hashicorp/vault/api v1.14.0 github.com/hashicorp/vault/sdk v0.8.1 github.com/moby/buildkit v0.31.2 - github.com/onsi/ginkgo/v2 v2.20.1 + github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.36.0 github.com/otiai10/copy v1.9.0 github.com/samber/lo v1.51.0 @@ -34,6 +34,9 @@ require ( github.com/werf/logboek v0.5.5 golang.org/x/sync v0.22.0 gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 ) require ( @@ -62,12 +65,14 @@ require ( github.com/evanphx/json-patch/v5 v5.5.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -78,6 +83,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.2 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -103,6 +109,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jellydator/ttlcache/v3 v3.4.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -115,8 +122,13 @@ require ( github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/moby/sys/signal v0.7.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -137,6 +149,7 @@ require ( github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect @@ -151,8 +164,10 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.39.0 // indirect @@ -162,10 +177,16 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect + k8s.io/klog/v2 v2.140.0 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) replace github.com/theupdateframework/go-tuf => github.com/werf/3p-go-tuf v0.0.0-20230315082915-5fc159235553 diff --git a/server/go.sum b/server/go.sum index 5503eff0..7b5c353c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -107,6 +107,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= @@ -121,6 +123,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -142,6 +146,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -150,6 +182,8 @@ github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -161,6 +195,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -178,6 +214,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -249,9 +287,13 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -298,6 +340,8 @@ github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc h1:dvhPFj1niuMP3CBCjhiZWJQr//+w1LOA8cHclFJnNe0= github.com/moby/policy-helpers v0.0.0-20260612073044-d5411a945cfc/go.mod h1:frGYJTxenVCGPa9doaqZSU9FqzT7bt+1dFeVAaCFoyQ= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= @@ -309,16 +353,24 @@ github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -434,12 +486,16 @@ github.com/werf/3p-go-tuf v0.0.0-20230315082915-5fc159235553 h1:ePJ7nTsNiLdeesH3 github.com/werf/3p-go-tuf v0.0.0-20230315082915-5fc159235553/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= github.com/werf/logboek v0.5.5 h1:RmtTejHJOyw0fub4pIfKsb7OTzD90ZOUyuBAXqYqJpU= github.com/werf/logboek v0.5.5/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -468,33 +524,50 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -512,15 +585,22 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= @@ -536,6 +616,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -550,5 +634,23 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/server/path_configure.go b/server/path_configure.go index 9d4c9f25..954f5e0d 100644 --- a/server/path_configure.go +++ b/server/path_configure.go @@ -36,6 +36,8 @@ const ( fieldNameBuildkitdAddress = "buildkitd_address" fieldNameBuildxDriver = "buildx_driver" fieldNameBuildxDriverOpts = "buildx_driver_opts" + fieldNameBuildkitdDriver = "buildkitd_driver" + fieldNameBuildkitdDriverOpts = "buildkitd_driver_opts" storageKeyConfiguration = "configuration" ) @@ -118,17 +120,27 @@ func configurePath(b *Backend) *framework.Path { }, fieldNameBuildkitdAddress: { Type: framework.TypeString, - Description: "An address of a running buildkitd (unix://, tcp://, docker-container:// or kube-pod:// scheme) to build release artifacts with the BuildKit client; the docker CLI is used if not set. Build secrets are sent to that daemon, and tcp:// is neither encrypted nor authenticated, so securing the channel and isolating the daemon is the administrator's responsibility", + Description: "An address of a running buildkitd (unix://, tcp://, docker-container:// or kube-pod:// scheme) to build release artifacts with the BuildKit client; the docker CLI is used only when neither this nor buildkitd_driver is set. Build secrets are sent to that daemon, and tcp:// is neither encrypted nor authenticated, so securing the channel and isolating the daemon is the administrator's responsibility", Required: false, }, fieldNameBuildxDriver: { Type: framework.TypeString, - Description: "The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable, and cannot be combined with buildkitd_address", + Description: "The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable, and cannot be combined with buildkitd_address or buildkitd_driver", Required: false, }, fieldNameBuildxDriverOpts: { Type: framework.TypeStringSlice, - Description: "The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables, and cannot be combined with buildkitd_address", + Description: "The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables, and cannot be combined with buildkitd_address or buildkitd_driver", + Required: false, + }, + fieldNameBuildkitdDriver: { + Type: framework.TypeString, + Description: "Provision an ephemeral buildkitd per build instead of using the docker CLI: kubernetes runs it as a pod and needs no docker binary next to the plugin. Cannot be combined with buildkitd_address, buildx_driver or buildx_driver_opts. A TRDL_BUILDKITD_ADDRESS set on the process wins over a stored driver, and the build reports the driver as unused", + Required: false, + }, + fieldNameBuildkitdDriverOpts: { + Type: framework.TypeStringSlice, + Description: "The buildkitd driver options, one name=value pair per element (e.g. namespace=trdl-build); they require buildkitd_driver to be set. The kubernetes driver accepts annotations, deadline, image, labels, limits.cpu, limits.ephemeral-storage, limits.memory, namespace, nodeselector, requests.cpu, requests.ephemeral-storage, requests.memory, rootless, serviceaccount and timeout; anything else is rejected", Required: false, }, }, @@ -153,6 +165,23 @@ func configurePath(b *Backend) *framework.Path { } } +func isConfigurationFieldSet(fields *framework.FieldData, name string) bool { + switch value := fields.Get(name).(type) { + case string: + return strings.TrimSpace(value) != "" + case []string: + return lo.SomeBy(value, func(item string) bool { return strings.TrimSpace(item) != "" }) + default: + panic(fmt.Sprintf("field %q has no emptiness rule", name)) + } +} + +func firstSetConfigurationField(fields *framework.FieldData, names ...string) string { + name, _ := lo.Find(names, func(name string) bool { return isConfigurationFieldSet(fields, name) }) + + return name +} + func (b *Backend) pathConfigureCreateOrUpdate(ctx context.Context, req *logical.Request, fields *framework.FieldData) (*logical.Response, error) { if errResp := util.CheckRequiredFields(req, fields); errResp != nil { return errResp, nil @@ -166,23 +195,29 @@ func (b *Backend) pathConfigureCreateOrUpdate(ctx context.Context, req *logical. return logical.ErrorResponse("%s validation failed: %s", fieldNameBuildxDriver, err), nil } - // A buildkitd address replaces the whole buildx path, so no builder is - // created and the driver settings would silently do nothing. Blank values - // mean "not set" here, exactly as they do when the settings are resolved. - if strings.TrimSpace(fields.Get(fieldNameBuildkitdAddress).(string)) != "" { - conflictingField := "" - if strings.TrimSpace(fields.Get(fieldNameBuildxDriver).(string)) != "" { - conflictingField = fieldNameBuildxDriver - } else if lo.SomeBy(fields.Get(fieldNameBuildxDriverOpts).([]string), func(opt string) bool { - return strings.TrimSpace(opt) != "" - }) { - conflictingField = fieldNameBuildxDriverOpts + if err := docker.ValidateBuildkitdDriver(ctx, fields.Get(fieldNameBuildkitdDriver).(string)); err != nil { + return logical.ErrorResponse("%s validation failed: %s", fieldNameBuildkitdDriver, err), nil + } + + // Each of the three build backends replaces the others entirely, so a setting + // belonging to one of the others would silently do nothing. Blank values mean + // "not set" here, exactly as they do when the settings are resolved. + if isConfigurationFieldSet(fields, fieldNameBuildkitdAddress) { + if conflictingField := firstSetConfigurationField(fields, fieldNameBuildxDriver, fieldNameBuildxDriverOpts, fieldNameBuildkitdDriver, fieldNameBuildkitdDriverOpts); conflictingField != "" { + return logical.ErrorResponse("%s cannot be combined with %s: no builder is provisioned when building against a buildkitd address", conflictingField, fieldNameBuildkitdAddress), nil } - if conflictingField != "" { - return logical.ErrorResponse("%s cannot be combined with %s: the buildx driver is not used when building against a buildkitd address", conflictingField, fieldNameBuildkitdAddress), nil + } + + if isConfigurationFieldSet(fields, fieldNameBuildkitdDriver) { + if conflictingField := firstSetConfigurationField(fields, fieldNameBuildxDriver, fieldNameBuildxDriverOpts); conflictingField != "" { + return logical.ErrorResponse("%s cannot be combined with %s: the docker CLI is not used when the plugin provisions buildkitd itself", conflictingField, fieldNameBuildkitdDriver), nil } } + if err := docker.ValidateBuildkitdDriverOpts(ctx, fields.Get(fieldNameBuildkitdDriver).(string), fields.Get(fieldNameBuildkitdDriverOpts).([]string)); err != nil { + return logical.ErrorResponse("%s validation failed: %s", fieldNameBuildkitdDriverOpts, err), nil + } + cfg := &configuration{ GitRepoUrl: fields.Get(fieldNameGitRepoUrl).(string), GitTrdlPath: fields.Get(fieldNameGitTrdlPath).(string), @@ -190,14 +225,16 @@ func (b *Backend) pathConfigureCreateOrUpdate(ctx context.Context, req *logical. GitTrdlChannelsBranch: fields.Get(fieldNameGitTrdlChannelsBranch).(string), InitialLastPublishedGitCommit: fields.Get(fieldNameInitialLastPublishedGitCommit).(string), RequiredNumberOfVerifiedSignaturesOnCommit: fields.Get(fieldNameRequiredNumberOfVerifiedSignaturesOnCommit).(int), - S3Endpoint: fields.Get(fieldNameS3Endpoint).(string), - S3Region: fields.Get(fieldNameS3Region).(string), - S3AccessKeyID: fields.Get(fieldNameS3AccessKeyID).(string), - S3SecretAccessKey: fields.Get(fieldNameS3SecretAccessKey).(string), - S3BucketName: fields.Get(fieldNameS3BucketName).(string), - BuildkitdAddress: fields.Get(fieldNameBuildkitdAddress).(string), - BuildxDriver: fields.Get(fieldNameBuildxDriver).(string), - BuildxDriverOpts: fields.Get(fieldNameBuildxDriverOpts).([]string), + S3Endpoint: fields.Get(fieldNameS3Endpoint).(string), + S3Region: fields.Get(fieldNameS3Region).(string), + S3AccessKeyID: fields.Get(fieldNameS3AccessKeyID).(string), + S3SecretAccessKey: fields.Get(fieldNameS3SecretAccessKey).(string), + S3BucketName: fields.Get(fieldNameS3BucketName).(string), + BuildkitdAddress: fields.Get(fieldNameBuildkitdAddress).(string), + BuildxDriver: fields.Get(fieldNameBuildxDriver).(string), + BuildxDriverOpts: fields.Get(fieldNameBuildxDriverOpts).([]string), + BuildkitdDriver: fields.Get(fieldNameBuildkitdDriver).(string), + BuildkitdDriverOpts: fields.Get(fieldNameBuildkitdDriverOpts).([]string), } if err := putConfiguration(ctx, req.Storage, cfg); err != nil { @@ -243,6 +280,8 @@ type configuration struct { BuildkitdAddress string `structs:"buildkitd_address" json:"buildkitd_address"` BuildxDriver string `structs:"buildx_driver" json:"buildx_driver"` BuildxDriverOpts []string `structs:"buildx_driver_opts" json:"buildx_driver_opts"` + BuildkitdDriver string `structs:"buildkitd_driver" json:"buildkitd_driver"` + BuildkitdDriverOpts []string `structs:"buildkitd_driver_opts" json:"buildkitd_driver_opts"` } func (cfg *configuration) RepositoryOptions() publisher.RepositoryOptions { diff --git a/server/path_configure_test.go b/server/path_configure_test.go index c70458fa..eccaba56 100644 --- a/server/path_configure_test.go +++ b/server/path_configure_test.go @@ -157,6 +157,8 @@ func dataCompleteConfiguration() map[string]interface{} { fieldNameBuildkitdAddress: cfg.BuildkitdAddress, fieldNameBuildxDriver: cfg.BuildxDriver, fieldNameBuildxDriverOpts: cfg.BuildxDriverOpts, + fieldNameBuildkitdDriver: cfg.BuildkitdDriver, + fieldNameBuildkitdDriverOpts: cfg.BuildkitdDriverOpts, } } @@ -170,6 +172,16 @@ func dataCompleteConfigurationWithoutBuildxFields() map[string]interface{} { return reqData } +// The buildkitd driver is mutually exclusive with the buildx pair too, so a +// request exercising it starts from the same reduced fixture. +func dataCompleteConfigurationWithBuildkitdDriver() map[string]interface{} { + reqData := dataCompleteConfigurationWithoutBuildxFields() + reqData[fieldNameBuildkitdDriver] = "kubernetes" + reqData[fieldNameBuildkitdDriverOpts] = []string{"namespace=trdl-build", "rootless=true"} + + return reqData +} + func completeConfiguration() *configuration { return &configuration{ GitRepoUrl: "https://github.com/werf/trdl/server.git", @@ -183,6 +195,7 @@ func completeConfiguration() *configuration { S3BucketName: "trdl", BuildxDriver: "kubernetes", BuildxDriverOpts: []string{"namespace=trdl-build", "nodeselector=disktype=ssd,zone=a"}, + BuildkitdDriverOpts: []string{}, } } @@ -203,13 +216,16 @@ func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_BuildxFieldsOmitted if assert.NotNil(suite.T(), cfg) { assert.Empty(suite.T(), cfg.BuildxDriver) assert.Empty(suite.T(), cfg.BuildxDriverOpts) + assert.Empty(suite.T(), cfg.BuildkitdDriver) } } func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_BuildxFieldsRejectedWithBuildkitdAddress() { for field, value := range map[string]interface{}{ - fieldNameBuildxDriver: "kubernetes", - fieldNameBuildxDriverOpts: []string{"namespace=trdl-build"}, + fieldNameBuildxDriver: "kubernetes", + fieldNameBuildxDriverOpts: []string{"namespace=trdl-build"}, + fieldNameBuildkitdDriver: "kubernetes", + fieldNameBuildkitdDriverOpts: []string{"namespace=trdl-build"}, } { conflictingField := field suite.Run(conflictingField, func() { @@ -254,6 +270,161 @@ func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_OmittedBuildxFields if assert.NotNil(suite.T(), cfg) { assert.Empty(suite.T(), cfg.BuildxDriver) assert.Empty(suite.T(), cfg.BuildxDriverOpts) + assert.Empty(suite.T(), cfg.BuildkitdDriver) + assert.Empty(suite.T(), cfg.BuildkitdDriverOpts) + } +} + +func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_BuildkitdDriverStored() { + suite.req.Operation = logical.CreateOperation + suite.req.Data = dataCompleteConfigurationWithBuildkitdDriver() + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), resp) + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), cfg) { + assert.Equal(suite.T(), "kubernetes", cfg.BuildkitdDriver) + assert.Equal(suite.T(), []string{"namespace=trdl-build", "rootless=true"}, cfg.BuildkitdDriverOpts) + } +} + +func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_UnsupportedBuildkitdDriverRejected() { + reqData := dataCompleteConfigurationWithBuildkitdDriver() + reqData[fieldNameBuildkitdDriver] = "nomad" + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), resp) { + assert.Contains(suite.T(), resp.Error().Error(), fieldNameBuildkitdDriver) + assert.Contains(suite.T(), resp.Error().Error(), "nomad") + } + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), cfg, "a rejected configuration must not be stored") +} + +// The whole point of a separate option vocabulary: an option the driver cannot +// honor is refused when the configuration is written, not by a release that +// fails hours later. +func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_UnsupportedBuildkitdDriverOptRejected() { + for _, driverOpt := range []string{"tolerations=key=node,operator=Exists", "replicas=3", "namespace"} { + rejectedOpt := driverOpt + suite.Run(rejectedOpt, func() { + reqData := dataCompleteConfigurationWithBuildkitdDriver() + reqData[fieldNameBuildkitdDriverOpts] = []string{rejectedOpt} + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), resp) { + assert.Contains(suite.T(), resp.Error().Error(), fieldNameBuildkitdDriverOpts) + } + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), cfg, "a rejected configuration must not be stored") + }) + } +} + +func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_BuildkitdDriverOptsRejectedWithoutDriver() { + reqData := dataCompleteConfigurationWithoutBuildxFields() + reqData[fieldNameBuildkitdDriverOpts] = []string{"namespace=trdl-build"} + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), resp) { + assert.Contains(suite.T(), resp.Error().Error(), fieldNameBuildkitdDriverOpts) + } + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), cfg, "a rejected configuration must not be stored") +} + +// Rejection has to happen before the storage write on the update path too: +// exercising it through create only would keep the suite green if the check +// moved below putConfiguration. +func (suite *PathConfigureCallbacksSuite) TestUpdate_RejectedBuildkitdDriverKeepsConfiguration() { + stored := completeConfiguration() + err := putConfiguration(suite.ctx, suite.storage, stored) + assert.Nil(suite.T(), err) + + for name, mutate := range map[string]func(map[string]interface{}){ + "unsupported driver": func(data map[string]interface{}) { + data[fieldNameBuildkitdDriver] = "nomad" + }, + "unsupported driver option": func(data map[string]interface{}) { + data[fieldNameBuildkitdDriver] = "kubernetes" + data[fieldNameBuildkitdDriverOpts] = []string{"replicas=3"} + }, + "driver options without a driver": func(data map[string]interface{}) { + data[fieldNameBuildkitdDriverOpts] = []string{"namespace=trdl-build"} + }, + "driver next to the buildx pair": func(data map[string]interface{}) { + data[fieldNameBuildkitdDriver] = "kubernetes" + data[fieldNameBuildxDriver] = "kubernetes" + }, + "driver next to an address": func(data map[string]interface{}) { + data[fieldNameBuildkitdAddress] = "tcp://buildkitd:1234" + data[fieldNameBuildkitdDriver] = "kubernetes" + }, + } { + rejected := mutate + suite.Run(name, func() { + reqData := dataCompleteConfigurationWithoutBuildxFields() + rejected(reqData) + + suite.req.Operation = logical.UpdateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), resp, "the update must be rejected") + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), stored, cfg, "a rejected update must leave the stored configuration untouched") + }) + } +} + +func (suite *PathConfigureCallbacksSuite) TestCreateOrUpdate_BuildxFieldsRejectedWithBuildkitdDriver() { + for field, value := range map[string]interface{}{ + fieldNameBuildxDriver: "kubernetes", + fieldNameBuildxDriverOpts: []string{"namespace=trdl-build"}, + } { + conflictingField := field + suite.Run(conflictingField, func() { + reqData := dataCompleteConfigurationWithBuildkitdDriver() + reqData[conflictingField] = value + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), resp) { + assert.Contains(suite.T(), resp.Error().Error(), conflictingField) + assert.Contains(suite.T(), resp.Error().Error(), fieldNameBuildkitdDriver) + } + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), cfg, "a rejected configuration must not be stored") + }) } } diff --git a/server/path_release.go b/server/path_release.go index d98734b3..d91f8998 100644 --- a/server/path_release.go +++ b/server/path_release.go @@ -165,14 +165,16 @@ func (b *Backend) pathRelease(ctx context.Context, req *logical.Request, fields go func() { err := docker.BuildReleaseArtifacts(ctx, docker.BuildReleaseArtifactsOpts{ - TarWriter: tarWriter, - GitRepo: gitRepo, - FromImage: trdlCfg.GetDockerImage(), - RunCommands: trdlCfg.Commands, - Storage: req.Storage, - BuildkitdAddress: cfg.BuildkitdAddress, - BuildxDriver: cfg.BuildxDriver, - BuildxDriverOpts: cfg.BuildxDriverOpts, + TarWriter: tarWriter, + GitRepo: gitRepo, + FromImage: trdlCfg.GetDockerImage(), + RunCommands: trdlCfg.Commands, + Storage: req.Storage, + BuildkitdAddress: cfg.BuildkitdAddress, + BuildxDriver: cfg.BuildxDriver, + BuildxDriverOpts: cfg.BuildxDriverOpts, + BuildkitdDriver: cfg.BuildkitdDriver, + BuildkitdDriverOpts: cfg.BuildkitdDriverOpts, }, b.Logger()) if err != nil { errCh <- err diff --git a/server/pkg/docker/build.go b/server/pkg/docker/build.go index f12fa499..2b6e44ab 100644 --- a/server/pkg/docker/build.go +++ b/server/pkg/docker/build.go @@ -25,14 +25,16 @@ const ( ) type BuildReleaseArtifactsOpts struct { - FromImage string - RunCommands []string - GitRepo *git.Repository - TarWriter *nio.PipeWriter - Storage logical.Storage - BuildkitdAddress string - BuildxDriver string - BuildxDriverOpts []string + FromImage string + RunCommands []string + GitRepo *git.Repository + TarWriter *nio.PipeWriter + Storage logical.Storage + BuildkitdAddress string + BuildxDriver string + BuildxDriverOpts []string + BuildkitdDriver string + BuildkitdDriverOpts []string } func BuildReleaseArtifacts(ctx context.Context, opts BuildReleaseArtifactsOpts, logger hclog.Logger) error { @@ -106,6 +108,8 @@ func BuildReleaseArtifacts(ctx context.Context, opts BuildReleaseArtifactsOpts, BuildkitdAddress: opts.BuildkitdAddress, BuildxDriver: opts.BuildxDriver, BuildxDriverOpts: opts.BuildxDriverOpts, + BuildkitdDriver: opts.BuildkitdDriver, + BuildkitdDriverOpts: opts.BuildkitdDriverOpts, Secrets: secrets, MacSigningCredentials: credentials, Logger: logger, diff --git a/server/pkg/docker/builder.go b/server/pkg/docker/builder.go index f1abb9ee..980d37db 100644 --- a/server/pkg/docker/builder.go +++ b/server/pkg/docker/builder.go @@ -30,6 +30,8 @@ const ( buildxDriverConfigurationSource = "the buildx_driver plugin configuration" defaultBuildxDriver = "docker-container" + + buildkitdDriverKubernetes = "kubernetes" ) // supportedBuildxDrivers are the drivers trdl has verified. The build streams a @@ -38,18 +40,24 @@ const ( // later with an opaque build failure. var supportedBuildxDrivers = []string{"docker-container", "kubernetes"} +// supportedBuildkitdDrivers are the ways trdl provisions a buildkitd of its own, +// needing no docker binary. Unset means it provisions none: the build either +// connects to buildkitd_address or goes through the docker CLI. +var supportedBuildkitdDrivers = []string{buildkitdDriverKubernetes} + type Logger interface { Info(msg string, args ...interface{}) Error(msg string, args ...interface{}) } type Builder struct { - builderName string - buildArgs []string - buildkitdAddress string - dockerfilePath string - secretsData map[string][]byte - logger Logger + builderName string + buildArgs []string + buildkitdAddress string + kubernetesBuilder *kubernetesBuilder + dockerfilePath string + secretsData map[string][]byte + logger Logger } type NewBuilderOpts struct { @@ -58,6 +66,8 @@ type NewBuilderOpts struct { BuildkitdAddress string BuildxDriver string BuildxDriverOpts []string + BuildkitdDriver string + BuildkitdDriverOpts []string Secrets []secrets.Secret MacSigningCredentials *mac_signing.Credentials Logger Logger @@ -69,14 +79,12 @@ func NewBuilder(ctx context.Context, opts *NewBuilderOpts) (*Builder, error) { return nil, err } if buildkitdAddress != "" { - // configure rejects both settings written together, but the address can - // also come from the environment, and then the driver settings are - // unreachable rather than rejected. Blank values mean "not set", as they - // do everywhere the settings are resolved. - if strings.TrimSpace(opts.BuildxDriver) != "" || lo.SomeBy(opts.BuildxDriverOpts, func(opt string) bool { - return strings.TrimSpace(opt) != "" - }) { - msg := fmt.Sprintf("Building against buildkitd at %q, the configured buildx driver settings are not used", buildkitdAddress) + // configure rejects these settings written together, but the address can + // also come from the environment, and then they are unreachable rather + // than rejected. Blank values mean "not set", as they do everywhere the + // settings are resolved. + if unused := unusedBuilderSettings(opts); len(unused) > 0 { + msg := fmt.Sprintf("Building against buildkitd at %q, the configured %s settings are not used", buildkitdAddress, strings.Join(unused, " and ")) logboek.Context(ctx).Default().LogLn(msg) opts.Logger.Info(msg) } @@ -91,6 +99,30 @@ func NewBuilder(ctx context.Context, opts *NewBuilderOpts) (*Builder, error) { builderName := fmt.Sprintf("trdl-builder-%s", opts.BuildId) + if strings.TrimSpace(opts.BuildkitdDriver) != "" { + // configure rejects the buildx fields written next to a buildkitd driver, + // but the environment has no such gate, so say what is being ignored + // instead of leaving it to be discovered from a builder that never appears. + if driver, source := resolveBuildxDriver(""); source != "the default" { + msg := fmt.Sprintf("Provisioning buildkitd with the %s driver, the buildx driver %q from %s is not used", opts.BuildkitdDriver, driver, source) + logboek.Context(ctx).Default().LogLn(msg) + opts.Logger.Info(msg) + } + + kubernetesBuilder, err := newKubernetesBuilder(ctx, builderName, opts.BuildkitdDriver, opts.BuildkitdDriverOpts, opts.Logger) + if err != nil { + return nil, err + } + + return &Builder{ + builderName: builderName, + kubernetesBuilder: kubernetesBuilder, + dockerfilePath: opts.DockerfilePathInContext, + secretsData: buildkitSecretsData(opts.Secrets, opts.MacSigningCredentials), + logger: opts.Logger, + }, nil + } + builderArgs, err := buildxCreateArgs(ctx, builderName, opts.BuildxDriver, opts.BuildxDriverOpts) if err != nil { return nil, fmt.Errorf("unable to construct buildx create args: %w", err) @@ -112,6 +144,21 @@ func NewBuilder(ctx context.Context, opts *NewBuilderOpts) (*Builder, error) { }, nil } +// unusedBuilderSettings names the settings a buildkitd address makes unreachable, +// so the build log says which knob is being ignored rather than leaving it to be +// discovered from a builder that never appears. +func unusedBuilderSettings(opts *NewBuilderOpts) []string { + var unused []string + if strings.TrimSpace(opts.BuildxDriver) != "" || len(trimDriverOpts(opts.BuildxDriverOpts)) > 0 { + unused = append(unused, "buildx driver") + } + if strings.TrimSpace(opts.BuildkitdDriver) != "" || len(trimDriverOpts(opts.BuildkitdDriverOpts)) > 0 { + unused = append(unused, "buildkitd driver") + } + + return unused +} + func buildxCreateArgs(ctx context.Context, builderName, configuredDriver string, configuredDriverOpts []string) ([]string, error) { driver, driverSource := resolveBuildxDriver(configuredDriver) if err := ValidateBuildxDriver(ctx, driver); err != nil { @@ -144,6 +191,46 @@ func ValidateBuildxDriver(ctx context.Context, driver string) error { return fmt.Errorf("unsupported driver %q (supported: %s)", driver, strings.Join(supportedBuildxDrivers, ", ")) } +// ValidateBuildkitdDriver accepts an empty driver, meaning trdl provisions no +// buildkitd of its own. +func ValidateBuildkitdDriver(ctx context.Context, driver string) error { + driver = strings.TrimSpace(driver) + if driver == "" || lo.Contains(supportedBuildkitdDrivers, driver) { + return nil + } + + return fmt.Errorf("unsupported buildkitd driver %q (supported: %s)", driver, strings.Join(supportedBuildkitdDrivers, ", ")) +} + +// ValidateBuildkitdDriverOpts rejects an option the driver cannot honor while +// the configuration is being written, rather than at release time. The options +// have no environment counterpart, so this check is reachable for every value +// that can ever arrive. +func ValidateBuildkitdDriverOpts(ctx context.Context, driver string, driverOpts []string) error { + driverOpts = trimDriverOpts(driverOpts) + if strings.TrimSpace(driver) == "" { + if len(driverOpts) > 0 { + return fmt.Errorf("buildkitd driver options are set without a buildkitd driver") + } + + return nil + } + + if _, err := parseKubernetesDriverOpts(driverOpts); err != nil { + return err + } + + return nil +} + +func trimDriverOpts(driverOpts []string) []string { + return lo.FilterMap(driverOpts, func(driverOpt string, _ int) (string, bool) { + trimmed := strings.TrimSpace(driverOpt) + + return trimmed, trimmed != "" + }) +} + // resolveBuildxDriver returns the driver to create the builder with and the // name of the setting it came from, so that a rejection points at the knob the // operator has to fix. @@ -220,6 +307,16 @@ func (b *Builder) Build(ctx context.Context, contextReader *nio.PipeReader, tarW return buildWithBuildkit(ctx, b.buildkitdAddress, b.dockerfilePath, b.secretsData, contextReader, tarWriter, b.logger) } + if b.kubernetesBuilder != nil { + bkClient, err := b.kubernetesBuilder.client(ctx) + if err != nil { + return err + } + defer bkClient.Close() + + return buildWithBuildkitClient(ctx, bkClient, b.dockerfilePath, b.secretsData, contextReader, tarWriter, b.logger) + } + finalArgs := append([]string{"buildx", "build"}, b.buildArgs...) cmd := exec.CommandContext(ctx, "docker", finalArgs...) @@ -246,6 +343,10 @@ func (b *Builder) Remove(ctx context.Context) error { return nil } + if b.kubernetesBuilder != nil { + return b.kubernetesBuilder.remove(ctx) + } + if err := runDockerCmd(ctx, []string{"buildx", "rm", b.builderName}); err != nil { return fmt.Errorf("unable to cleanup: %w", err) } diff --git a/server/pkg/docker/buildkit.go b/server/pkg/docker/buildkit.go index 78c06a39..d7f7d830 100644 --- a/server/pkg/docker/buildkit.go +++ b/server/pkg/docker/buildkit.go @@ -107,6 +107,10 @@ func buildWithBuildkit(ctx context.Context, address, dockerfilePath string, secr } defer bkClient.Close() + return buildWithBuildkitClient(ctx, bkClient, dockerfilePath, secretsData, contextReader, tarWriter, logger) +} + +func buildWithBuildkitClient(ctx context.Context, bkClient *bkclient.Client, dockerfilePath string, secretsData map[string][]byte, contextReader io.ReadCloser, tarWriter io.WriteCloser, logger Logger) error { contextUploader := uploadprovider.New() solveOpt := bkclient.SolveOpt{ Frontend: "dockerfile.v0", diff --git a/server/pkg/docker/buildkit_test.go b/server/pkg/docker/buildkit_test.go index 741d773c..913f6180 100644 --- a/server/pkg/docker/buildkit_test.go +++ b/server/pkg/docker/buildkit_test.go @@ -247,6 +247,27 @@ func TestNewBuilder_ReportsUnusedBuildxSettingsInBuildkitMode(t *testing.T) { assert.Contains(t, strings.Join(logger.lines, "\n"), "buildx driver settings are not used") } +func TestNewBuilder_ReportsUnusedBuildkitdDriverInBuildkitMode(t *testing.T) { + t.Setenv(buildkitdAddressEnv, "tcp://buildkitd:1234") + logger := &recordingLogger{} + + builder, err := NewBuilder(context.Background(), &NewBuilderOpts{ + BuildId: "42", + DockerfilePathInContext: ".trdl/Dockerfile", + BuildkitdDriver: "kubernetes", + BuildkitdDriverOpts: []string{"namespace=trdl-build"}, + Logger: logger, + }) + + require.NoError(t, err) + assert.Equal(t, "tcp://buildkitd:1234", builder.buildkitdAddress) + assert.Nil(t, builder.kubernetesBuilder, "no builder is provisioned when an address is set") + + logger.mu.Lock() + defer logger.mu.Unlock() + assert.Contains(t, strings.Join(logger.lines, "\n"), "buildkitd driver settings are not used") +} + func TestBuild_UnblocksContextProducerWhenBuildFails(t *testing.T) { // A one-byte pipe buffer puts the producer in the state a real context reaches // once it outgrows the buffer: blocked on write until the build drains it. diff --git a/server/pkg/docker/kubernetes.go b/server/pkg/docker/kubernetes.go new file mode 100644 index 00000000..e6bdccef --- /dev/null +++ b/server/pkg/docker/kubernetes.go @@ -0,0 +1,542 @@ +package docker + +import ( + "context" + "fmt" + "net" + "strconv" + "strings" + "time" + + bkclient "github.com/moby/buildkit/client" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/werf/logboek" +) + +const ( + buildkitContainerName = "buildkitd" + defaultBuildkitImage = "moby/buildkit:buildx-stable-1" + defaultRootlessBuildkitImage = defaultBuildkitImage + "-rootless" + defaultBuildkitNamespace = "default" + + // Matches the buildx kubernetes driver's own default for the same wait. + defaultBuildkitPodTimeout = 2 * time.Minute + buildkitPodPollInterval = time.Second + buildkitPodCleanupTimeout = 30 * time.Second +) + +// supportedKubernetesDriverOpts is this driver's own vocabulary. The names match +// the buildx kubernetes driver wherever the two overlap, but the buildx path +// passes its options through untouched while these are applied here, so one this +// driver cannot honor is rejected rather than dropped silently. +var supportedKubernetesDriverOpts = []string{ + "annotations", + "deadline", + "image", + "labels", + "limits.cpu", + "limits.ephemeral-storage", + "limits.memory", + "namespace", + "nodeselector", + "requests.cpu", + "requests.ephemeral-storage", + "requests.memory", + "rootless", + "serviceaccount", + "timeout", +} + +type kubernetesBuilderOpts struct { + namespace string + image string + rootless bool + serviceAccountName string + nodeSelector map[string]string + labels map[string]string + annotations map[string]string + requests corev1.ResourceList + limits corev1.ResourceList + deadline time.Duration + timeout time.Duration +} + +type kubernetesBuilder struct { + restClient rest.Interface + restConfig *rest.Config + namespace string + podName string + readyTimeout time.Duration + logger Logger +} + +func newKubernetesBuilder(ctx context.Context, builderName, driver string, driverOpts []string, logger Logger) (*kubernetesBuilder, error) { + if err := ValidateBuildkitdDriver(ctx, driver); err != nil { + return nil, err + } + + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) + + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("unable to configure the kubernetes client: %w", err) + } + + restClient, err := newCoreRESTClient(restConfig) + if err != nil { + return nil, fmt.Errorf("unable to create the kubernetes client: %w", err) + } + + opts, err := parseKubernetesDriverOpts(trimDriverOpts(driverOpts)) + if err != nil { + return nil, fmt.Errorf("unable to parse the buildkitd driver options: %w", err) + } + if opts.namespace == "" { + opts.namespace = namespaceFromClientConfig(clientConfig) + } + + b := &kubernetesBuilder{ + restClient: restClient, + restConfig: restConfig, + namespace: opts.namespace, + podName: builderName, + readyTimeout: opts.timeout, + logger: logger, + } + + if err := b.bootstrap(ctx, opts); err != nil { + return nil, err + } + + return b, nil +} + +// bootstrap removes whatever it created before returning an error, so a failure +// between creating the pod and it becoming ready cannot leave a builder running. +func (b *kubernetesBuilder) bootstrap(ctx context.Context, opts kubernetesBuilderOpts) error { + pod := buildkitPod(b.podName, opts) + + if err := b.createPod(ctx, pod); err != nil { + // The API server may have persisted the pod before the response was lost, + // and the caller gets no builder back to clean up with, so the delete is + // attempted here too. Usually nothing was created and it is a no-op, but + // when both calls fail the operator has a privileged pod to find by hand + // and has to be told so, the same way the readiness path tells them. + if removeErr := b.removeAfterFailure(ctx); removeErr != nil { + return fmt.Errorf("unable to create builder pod %s/%s: %w (a pod may have been created and was not removed: %w)", b.namespace, b.podName, err, removeErr) + } + + return fmt.Errorf("unable to create builder pod %s/%s: %w", b.namespace, b.podName, err) + } + + b.log(ctx, fmt.Sprintf("Waiting for builder pod %s/%s", b.namespace, b.podName)) + + if err := b.waitForPod(ctx); err != nil { + if removeErr := b.removeAfterFailure(ctx); removeErr != nil { + return fmt.Errorf("%w (the builder pod was left behind: %w)", err, removeErr) + } + + return err + } + + return nil +} + +// removeAfterFailure deletes the pod on a path where the caller never receives a +// builder and so cannot run the cleanup itself. A canceled build context is +// exactly when this runs, so the delete is given one of its own. +func (b *kubernetesBuilder) removeAfterFailure(ctx context.Context) error { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), buildkitPodCleanupTimeout) + defer cancel() + + return b.remove(cleanupCtx) +} + +// The pod's own readiness probe runs `buildctl debug workers`, so a ready pod is +// one already serving builds. Connecting earlier reaches a buildkitd that is not. +func (b *kubernetesBuilder) waitForPod(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, b.readyTimeout) + defer cancel() + + ticker := time.NewTicker(buildkitPodPollInterval) + defer ticker.Stop() + + lastState := "" + for { + pod, err := b.getPod(ctx) + if err != nil { + if ctx.Err() != nil { + return fmt.Errorf("builder pod %s/%s is not ready: %w (last state: %s, last error: %v)", b.namespace, b.podName, ctx.Err(), lo.Ternary(lastState == "", "unknown", lastState), err) + } + + // A single failed read is not a failed build: an API server rolling a + // replica, a 429 from a fairness queue or a token rotation all produce + // one, and the wait exists to ride them out. Keep polling until the + // deadline, and report the last error with it. + lastState = fmt.Sprintf("unreadable (%v)", err) + + select { + case <-ctx.Done(): + return fmt.Errorf("builder pod %s/%s is not ready: %w (last state: %s)", b.namespace, b.podName, ctx.Err(), lastState) + case <-ticker.C: + } + + continue + } + + if pod.Status.Phase == corev1.PodFailed || pod.Status.Phase == corev1.PodSucceeded { + return fmt.Errorf("builder pod %s/%s terminated with phase %s: %s", b.namespace, b.podName, pod.Status.Phase, pod.Status.Reason) + } + if isPodReady(pod) { + return nil + } + lastState = podState(pod) + + select { + case <-ctx.Done(): + return fmt.Errorf("builder pod %s/%s is not ready: %w (last state: %s)", b.namespace, b.podName, ctx.Err(), lastState) + case <-ticker.C: + } + } +} + +func (b *kubernetesBuilder) client(ctx context.Context) (*bkclient.Client, error) { + client, err := bkclient.New(ctx, "", bkclient.WithContextDialer(b.dialerFor(ctx))) + if err != nil { + return nil, fmt.Errorf("unable to connect to the builder pod %s/%s: %w", b.namespace, b.podName, err) + } + + return client, nil +} + +// dialerFor drops the context gRPC hands the dialer, deliberately: gRPC scopes +// it to one connection attempt and cancels it as soon as the transport is up, +// which would tear down the exec stream the connection is made of. The stream +// has to live as long as the build, so it follows the build's context. +func (b *kubernetesBuilder) dialerFor(ctx context.Context) func(context.Context, string) (net.Conn, error) { + return func(context.Context, string) (net.Conn, error) { + return b.dial(ctx) + } +} + +func (b *kubernetesBuilder) remove(ctx context.Context) error { + if err := b.deletePod(ctx); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("unable to delete builder pod %s/%s: %w", b.namespace, b.podName, err) + } + + return nil +} + +// The pod calls go through a plain REST client rather than the generated typed +// client, which reaches every API group and its apply configurations: linking it +// costs the plugin binary tens of megabytes to create one pod. +func newCoreRESTClient(restConfig *rest.Config) (rest.Interface, error) { + config := rest.CopyConfig(restConfig) + config.APIPath = "/api" + config.GroupVersion = &corev1.SchemeGroupVersion + config.NegotiatedSerializer = serializer.NewCodecFactory(coreScheme) + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return rest.RESTClientFor(config) +} + +func (b *kubernetesBuilder) createPod(ctx context.Context, pod *corev1.Pod) error { + return b.restClient.Post(). + Namespace(b.namespace). + Resource("pods"). + Body(pod). + Do(ctx). + Error() +} + +func (b *kubernetesBuilder) getPod(ctx context.Context) (*corev1.Pod, error) { + pod := &corev1.Pod{} + if err := b.restClient.Get(). + Namespace(b.namespace). + Resource("pods"). + Name(b.podName). + Do(ctx). + Into(pod); err != nil { + return nil, err + } + + return pod, nil +} + +func (b *kubernetesBuilder) deletePod(ctx context.Context) error { + return b.restClient.Delete(). + Namespace(b.namespace). + Resource("pods"). + Name(b.podName). + Do(ctx). + Error() +} + +// coreScheme carries core/v1 alone. client-go's own kubernetes/scheme registers +// every API group, which links the whole generated surface into the plugin. +var coreScheme = newCoreScheme() + +func newCoreScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + lo.Must0(corev1.AddToScheme(scheme)) + + return scheme +} + +func (b *kubernetesBuilder) log(ctx context.Context, msg string) { + logboek.Context(ctx).Default().LogLn(msg) + b.logger.Info(msg) +} + +func isPodReady(pod *corev1.Pod) bool { + if pod.Status.Phase != corev1.PodRunning { + return false + } + + condition, found := lo.Find(pod.Status.Conditions, func(c corev1.PodCondition) bool { + return c.Type == corev1.PodReady + }) + + return found && condition.Status == corev1.ConditionTrue +} + +// podState is only ever shown to an operator whose build is stuck waiting, so it +// reports whatever the pod says about itself rather than a fixed set of reasons. +func podState(pod *corev1.Pod) string { + for _, status := range pod.Status.ContainerStatuses { + if status.State.Waiting != nil && status.State.Waiting.Reason != "" { + return fmt.Sprintf("%s (%s)", pod.Status.Phase, status.State.Waiting.Reason) + } + } + + return string(pod.Status.Phase) +} + +func namespaceFromClientConfig(clientConfig clientcmd.ClientConfig) string { + namespace, _, err := clientConfig.Namespace() + if err != nil || strings.TrimSpace(namespace) == "" { + return defaultBuildkitNamespace + } + + return namespace +} + +func parseKubernetesDriverOpts(driverOpts []string) (kubernetesBuilderOpts, error) { + opts := kubernetesBuilderOpts{ + nodeSelector: map[string]string{}, + labels: map[string]string{}, + annotations: map[string]string{}, + requests: corev1.ResourceList{}, + limits: corev1.ResourceList{}, + } + + imageSet, timeoutSet, deadlineSet := false, false, false + for _, driverOpt := range driverOpts { + name, value, found := strings.Cut(driverOpt, "=") + if !found { + return opts, fmt.Errorf("driver option %q is not a name=value pair", driverOpt) + } + name = strings.TrimSpace(name) + + var err error + switch name { + case "namespace": + opts.namespace = strings.TrimSpace(value) + case "image": + // A blank value means "not set" here as everywhere else, so it must not + // suppress the rootless default and store an image the API server will + // refuse. + opts.image = strings.TrimSpace(value) + imageSet = opts.image != "" + case "serviceaccount": + opts.serviceAccountName = strings.TrimSpace(value) + case "rootless": + opts.rootless, err = strconv.ParseBool(value) + case "deadline": + deadlineSet = true + opts.deadline, err = time.ParseDuration(value) + case "timeout": + timeoutSet = true + opts.timeout, err = time.ParseDuration(value) + case "nodeselector": + err = mergeKeyValues(opts.nodeSelector, value) + case "labels": + err = mergeKeyValues(opts.labels, value) + case "annotations": + err = mergeKeyValues(opts.annotations, value) + case "requests.cpu", "requests.memory", "requests.ephemeral-storage": + err = setResourceQuantity(opts.requests, strings.TrimPrefix(name, "requests."), value) + case "limits.cpu", "limits.memory", "limits.ephemeral-storage": + err = setResourceQuantity(opts.limits, strings.TrimPrefix(name, "limits."), value) + default: + return opts, fmt.Errorf("unsupported option %q for the %s buildkitd driver (supported: %s)", name, buildkitdDriverKubernetes, strings.Join(supportedKubernetesDriverOpts, ", ")) + } + if err != nil { + return opts, fmt.Errorf("driver option %q: %w", name, err) + } + } + + if !imageSet { + opts.image = lo.Ternary(opts.rootless, defaultRootlessBuildkitImage, defaultBuildkitImage) + } + if !timeoutSet { + opts.timeout = defaultBuildkitPodTimeout + } + // activeDeadlineSeconds is whole seconds and must be at least one, so a + // sub-second deadline would truncate to zero and a fractional one would + // silently lose its remainder. + if deadlineSet && (opts.deadline < time.Second || opts.deadline%time.Second != 0) { + return opts, fmt.Errorf("driver option %q: must be a whole number of seconds, at least 1s", "deadline") + } + if opts.timeout <= 0 { + return opts, fmt.Errorf("driver option %q: must be positive", "timeout") + } + for name, request := range opts.requests { + limit, ok := opts.limits[name] + if ok && request.Cmp(limit) > 0 { + return opts, fmt.Errorf("driver option %q: %s exceeds the %s limit of %s", "requests."+string(name), request.String(), name, limit.String()) + } + } + + return opts, nil +} + +func setResourceQuantity(list corev1.ResourceList, name, value string) error { + quantity, err := resource.ParseQuantity(strings.TrimSpace(value)) + if err != nil { + return err + } + // ParseQuantity accepts a signed quantity, and a negative one is only rejected + // by the API server when the build finally tries to create the pod. + if quantity.Sign() < 0 { + return fmt.Errorf("must not be negative") + } + list[corev1.ResourceName(name)] = quantity + + return nil +} + +// mergeKeyValues adds into the map rather than replacing it: the options are one +// name=value pair per element, so an operator naturally writes the same option +// twice, and replacing would drop the earlier pairs without an error anywhere. +func mergeKeyValues(into map[string]string, value string) error { + parsed, err := splitKeyValues(value) + if err != nil { + return err + } + for k, v := range parsed { + into[k] = v + } + + return nil +} + +func splitKeyValues(value string) (map[string]string, error) { + result := map[string]string{} + for _, pair := range strings.Split(value, ",") { + if strings.TrimSpace(pair) == "" { + continue + } + + name, v, found := strings.Cut(pair, "=") + if !found { + return nil, fmt.Errorf("%q is not a name=value pair", pair) + } + result[strings.TrimSpace(name)] = strings.TrimSpace(v) + } + + return result, nil +} + +func buildkitPod(name string, opts kubernetesBuilderOpts) *corev1.Pod { + labels := map[string]string{"app": name} + for k, v := range opts.labels { + labels[k] = v + } + + annotations := map[string]string{} + for k, v := range opts.annotations { + annotations[k] = v + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: opts.namespace, + Name: name, + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + ServiceAccountName: opts.serviceAccountName, + NodeSelector: opts.nodeSelector, + Containers: []corev1.Container{ + { + Name: buildkitContainerName, + Image: opts.image, + SecurityContext: &corev1.SecurityContext{ + Privileged: lo.ToPtr(true), + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"buildctl", "debug", "workers"}, + }, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: opts.requests, + Limits: opts.limits, + }, + }, + }, + }, + } + + if opts.deadline > 0 { + pod.Spec.ActiveDeadlineSeconds = lo.ToPtr(int64(opts.deadline.Seconds())) + } + if opts.rootless { + toRootless(pod) + } + + return pod +} + +// Rootless BuildKit needs the whole set: dropping the seccomp profile or the +// AppArmor annotation gives a pod that starts and then fails every build. +func toRootless(pod *corev1.Pod) { + container := &pod.Spec.Containers[0] + + container.Args = append(container.Args, "--oci-worker-no-process-sandbox") + container.SecurityContext = &corev1.SecurityContext{ + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}, + // The field is the supported API; the annotation below is deprecated since + // Kubernetes 1.30 and carried only so that clusters older than that still + // see the profile. + AppArmorProfile: &corev1.AppArmorProfile{Type: corev1.AppArmorProfileTypeUnconfined}, + } + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: buildkitContainerName, + // The image declares this path as a VOLUME, which rootless cannot use on + // hosts mounting it nosuid,nodev; an emptyDir replaces it. + MountPath: "/home/user/.local/share/buildkit", + }) + + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: buildkitContainerName, + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, + }) + pod.Annotations["container.apparmor.security.beta.kubernetes.io/"+buildkitContainerName] = "unconfined" +} diff --git a/server/pkg/docker/kubernetes_exec.go b/server/pkg/docker/kubernetes_exec.go new file mode 100644 index 00000000..70ff7c7f --- /dev/null +++ b/server/pkg/docker/kubernetes_exec.go @@ -0,0 +1,134 @@ +package docker + +import ( + "context" + "fmt" + "io" + "net" + "sync" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/remotecommand" +) + +var execParameterCodec = runtime.NewParameterCodec(coreScheme) + +// dial reaches buildkitd through the API server's pods/exec channel rather than +// over the network, so the plugin needs no route to the builder pod itself. The +// context has to be the build's, not the one gRPC hands its dialer — see client. +func (b *kubernetesBuilder) dial(ctx context.Context) (net.Conn, error) { + request := b.restClient. + Post(). + Namespace(b.namespace). + Resource("pods"). + Name(b.podName). + SubResource("exec"). + VersionedParams(&corev1.PodExecOptions{ + Container: buildkitContainerName, + Command: []string{"buildctl", "dial-stdio"}, + Stdin: true, + Stdout: true, + Stderr: true, + }, execParameterCodec) + + executor, err := remotecommand.NewSPDYExecutor(b.restConfig, "POST", request.URL()) + if err != nil { + return nil, fmt.Errorf("unable to set up an exec stream to %s/%s: %w", b.namespace, b.podName, err) + } + + stdinReader, stdinWriter := io.Pipe() + stdoutReader, stdoutWriter := io.Pipe() + + go func() { + logPipe, waitForLogs := logWriter(b.logger) + defer waitForLogs() + + streamErr := executor.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdin: stdinReader, + Stdout: stdoutWriter, + Stderr: logPipe, + }) + + // Closing both ends with the stream error fails a caller blocked on the + // connection instead of leaving it waiting for bytes that cannot arrive. + stdoutWriter.CloseWithError(streamErr) + stdinReader.CloseWithError(streamErr) + }() + + return &execConn{stdin: stdinWriter, stdout: stdoutReader}, nil +} + +var _ net.Conn = (*execConn)(nil) + +// execConn adapts one exec stream to net.Conn. CloseWrite and CloseRead are real +// rather than decorative so that a half-close reaches the stream, but note what +// does NOT depend on them: gRPC never calls either — it closes the connection +// outright — and buildkit's own half-closes happen inside the pod, on buildctl's +// stdio. Deadlines are no-ops, which costs the shutdown guard in gRPC's +// http2Client.Close a few seconds of its own fallback timer on a stalled stream. +type execConn struct { + stdin *io.PipeWriter + stdout *io.PipeReader + + closedMu sync.Mutex + stdinClosed bool + stdoutClosed bool +} + +func (c *execConn) Read(p []byte) (int, error) { + return c.stdout.Read(p) +} + +func (c *execConn) Write(p []byte) (int, error) { + return c.stdin.Write(p) +} + +func (c *execConn) CloseWrite() error { + c.closedMu.Lock() + c.stdinClosed = true + c.closedMu.Unlock() + + return c.stdin.Close() +} + +func (c *execConn) CloseRead() error { + c.closedMu.Lock() + c.stdoutClosed = true + c.closedMu.Unlock() + + return c.stdout.Close() +} + +func (c *execConn) Close() error { + c.closedMu.Lock() + stdinClosed, stdoutClosed := c.stdinClosed, c.stdoutClosed + c.closedMu.Unlock() + + var err error + if !stdinClosed { + err = c.CloseWrite() + } + if !stdoutClosed { + if closeErr := c.CloseRead(); err == nil { + err = closeErr + } + } + + return err +} + +func (c *execConn) LocalAddr() net.Addr { return execAddr("local") } +func (c *execConn) RemoteAddr() net.Addr { return execAddr("remote") } + +// The stream carries no deadline of its own; canceling the build context is +// what ends it. +func (c *execConn) SetDeadline(time.Time) error { return nil } +func (c *execConn) SetReadDeadline(time.Time) error { return nil } +func (c *execConn) SetWriteDeadline(time.Time) error { return nil } + +type execAddr string + +func (a execAddr) Network() string { return "pods/exec" } +func (a execAddr) String() string { return string(a) } diff --git a/server/pkg/docker/kubernetes_test.go b/server/pkg/docker/kubernetes_test.go new file mode 100644 index 00000000..0a1338e0 --- /dev/null +++ b/server/pkg/docker/kubernetes_test.go @@ -0,0 +1,519 @@ +package docker + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" + + "github.com/werf/logboek" +) + +// fakeAPIServer answers the three pod calls the builder makes. Tests drive a +// builder against it instead of a cluster, so a mutation that removes a guard +// reaches this server and fails the assertion rather than provisioning anything. +type fakeAPIServer struct { + *httptest.Server + + mu sync.Mutex + calls []string + created *corev1.Pod + deleted bool + readyPod bool + phase corev1.PodPhase + failCreate bool + failDelete bool +} + +func newFakeAPIServer(t *testing.T) *fakeAPIServer { + f := &fakeAPIServer{} + f.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + defer f.mu.Unlock() + + f.calls = append(f.calls, r.Method+" "+r.URL.RequestURI()) + w.Header().Set("Content-Type", "application/json") + + // The exec subresource is a POST with no JSON body and a SPDY upgrade this + // server does not speak; it is recorded and refused, which is enough to + // show the request was issued at all. + if strings.HasSuffix(r.URL.Path, "/exec") { + w.WriteHeader(http.StatusBadRequest) + + return + } + + switch r.Method { + case http.MethodPost: + pod := &corev1.Pod{} + require.NoError(t, json.NewDecoder(r.Body).Decode(pod)) + // The pod is recorded before the status is written, so failCreate + // reproduces the case where the API server persisted it and the + // client saw only an error. + f.created = pod + if f.failCreate { + w.WriteHeader(http.StatusInternalServerError) + require.NoError(t, json.NewEncoder(w).Encode(&metav1.Status{Status: metav1.StatusFailure, Code: http.StatusInternalServerError})) + + return + } + w.WriteHeader(http.StatusCreated) + require.NoError(t, json.NewEncoder(w).Encode(pod)) + case http.MethodGet: + if f.created == nil || f.deleted { + writeNotFound(t, w) + + return + } + require.NoError(t, json.NewEncoder(w).Encode(podWithStatus(f.created, f.readyPod, f.phase))) + case http.MethodDelete: + if f.failDelete { + w.WriteHeader(http.StatusInternalServerError) + require.NoError(t, json.NewEncoder(w).Encode(&metav1.Status{Status: metav1.StatusFailure, Code: http.StatusInternalServerError})) + + return + } + if f.created == nil || f.deleted { + writeNotFound(t, w) + + return + } + f.deleted = true + require.NoError(t, json.NewEncoder(w).Encode(&metav1.Status{Status: metav1.StatusSuccess})) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + })) + t.Cleanup(f.Close) + + return f +} + +func writeNotFound(t *testing.T, w http.ResponseWriter) { + t.Helper() + + w.WriteHeader(http.StatusNotFound) + require.NoError(t, json.NewEncoder(w).Encode(&metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + })) +} + +func podWithStatus(pod *corev1.Pod, ready bool, phase corev1.PodPhase) *corev1.Pod { + result := pod.DeepCopy() + result.Status.Phase = corev1.PodPending + if phase != "" { + result.Status.Phase = phase + + return result + } + if ready { + result.Status.Phase = corev1.PodRunning + result.Status.Conditions = []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}} + } + + return result +} + +func (f *fakeAPIServer) methods() []string { + f.mu.Lock() + defer f.mu.Unlock() + + methods := make([]string, 0, len(f.calls)) + for _, call := range f.calls { + methods = append(methods, strings.Fields(call)[0]) + } + + return methods +} + +func (f *fakeAPIServer) recordedCalls() []string { + f.mu.Lock() + defer f.mu.Unlock() + + return append([]string(nil), f.calls...) +} + +func (f *fakeAPIServer) podDeleted() bool { + f.mu.Lock() + defer f.mu.Unlock() + + return f.deleted +} + +func newTestBuilder(t *testing.T, f *fakeAPIServer, readyTimeout time.Duration) *kubernetesBuilder { + t.Helper() + + // This path provisions the builder itself and must never reach for a binary. + // An empty PATH keeps a mutation that reintroduces one from finding a real + // docker or kubectl on the machine running the tests. + t.Setenv("PATH", "") + + config := &rest.Config{Host: f.URL} + config.APIPath = "/api" + config.GroupVersion = &corev1.SchemeGroupVersion + config.NegotiatedSerializer = serializer.NewCodecFactory(coreScheme) + + restClient, err := rest.RESTClientFor(config) + require.NoError(t, err) + + return &kubernetesBuilder{ + restClient: restClient, + restConfig: config, + namespace: "trdl-build", + podName: "trdl-builder-42", + readyTimeout: readyTimeout, + logger: smokeLogger{t}, + } +} + +func testContext() context.Context { + return logboek.NewContext(context.Background(), logboek.DefaultLogger()) +} + +func testBuilderOpts() kubernetesBuilderOpts { + opts, err := parseKubernetesDriverOpts([]string{"namespace=trdl-build"}) + if err != nil { + panic(err) + } + + return opts +} + +func TestKubernetesBuilderBootstrapReady(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = true + + b := newTestBuilder(t, f, time.Minute) + + require.NoError(t, b.bootstrap(testContext(), testBuilderOpts())) + assert.False(t, f.podDeleted(), "a builder that came up must not be removed by bootstrap") + assert.Equal(t, []string{"POST", "GET"}, f.methods()) +} + +// The pod must not outlive a bootstrap that failed: the caller gets no builder +// back, so nothing else is in a position to remove it. +func TestKubernetesBuilderBootstrapRemovesPodThatNeverBecomesReady(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = false + + b := newTestBuilder(t, f, 100*time.Millisecond) + + err := b.bootstrap(testContext(), testBuilderOpts()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "not ready") + assert.True(t, f.podDeleted(), "the builder pod must be removed when it never becomes ready") +} + +// Canceling the release is the case the cleanup exists for, and the case where +// reusing the build context would silently skip the delete. +func TestKubernetesBuilderBootstrapRemovesPodOnCancelledContext(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = false + + b := newTestBuilder(t, f, time.Minute) + + ctx, cancel := context.WithCancel(testContext()) + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + + err := b.bootstrap(ctx, testBuilderOpts()) + + require.Error(t, err) + assert.True(t, f.podDeleted(), "the builder pod must be removed after the build context is canceled") +} + +// A Running pod is not a serving one: buildkitd reports itself through the +// readiness probe, and connecting before it passes reaches a daemon that is not +// yet accepting builds. +func TestKubernetesBuilderBootstrapWaitsForReadinessNotJustRunning(t *testing.T) { + f := newFakeAPIServer(t) + f.phase = corev1.PodRunning + + b := newTestBuilder(t, f, 100*time.Millisecond) + + err := b.bootstrap(testContext(), testBuilderOpts()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "not ready") + assert.True(t, f.podDeleted(), "the builder pod must be removed when readiness never arrives") +} + +// A pod the API server persisted while the client saw a failure is invisible to +// the caller — no builder is returned, so nothing else can clean it up. +func TestKubernetesBuilderBootstrapRemovesPodAfterFailedCreate(t *testing.T) { + f := newFakeAPIServer(t) + f.failCreate = true + + b := newTestBuilder(t, f, time.Minute) + + err := b.bootstrap(testContext(), testBuilderOpts()) + + require.Error(t, err) + assert.True(t, f.podDeleted(), "a pod that may have been created must be deleted when create reports failure") + // The forwarding guard in CI greps this message for the namespace the driver + // options carried, so the namespace has to be in it. + assert.Contains(t, err.Error(), "builder pod trdl-build/", "the error must name the namespace the builder was configured with") +} + +// When the create fails and the cleanup fails too, the operator has a privileged +// pod nobody can see and has to be told: the create error alone does not say a +// pod may exist. +func TestKubernetesBuilderBootstrapReportsAPodItCouldNotRemove(t *testing.T) { + f := newFakeAPIServer(t) + f.failCreate = true + f.failDelete = true + + b := newTestBuilder(t, f, time.Minute) + + err := b.bootstrap(testContext(), testBuilderOpts()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to create builder pod") + assert.Contains(t, err.Error(), "was not removed", "a pod that may exist and could not be deleted has to be named") +} + +// gRPC cancels the context it hands a dialer as soon as the transport is up. If +// the exec stream were scoped to that context it would be torn down immediately, +// which is what made the first cluster run fail with "context canceled". The +// stream must follow the build's context instead: with a dead dialer context the +// request still has to reach the API server. +func TestKubernetesBuilderClientIgnoresDialerContext(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = true + + b := newTestBuilder(t, f, time.Minute) + + dead, cancel := context.WithCancel(context.Background()) + cancel() + + conn, err := b.dialerFor(testContext())(dead, "") + if err == nil { + t.Cleanup(func() { _ = conn.Close() }) + } + + assert.Eventually(t, func() bool { + for _, call := range f.recordedCalls() { + if strings.Contains(call, "/exec") { + return true + } + } + + return false + }, 5*time.Second, 20*time.Millisecond, "the exec request must be issued even when the dialer context is already canceled") +} + +func TestKubernetesBuilderRemoveToleratesMissingPod(t *testing.T) { + f := newFakeAPIServer(t) + b := newTestBuilder(t, f, time.Minute) + + assert.NoError(t, b.remove(testContext()), "removing a pod that is already gone is not a failure") +} + +func TestKubernetesBuilderBootstrapRemovesTerminatedPod(t *testing.T) { + f := newFakeAPIServer(t) + f.phase = corev1.PodFailed + + b := newTestBuilder(t, f, time.Minute) + + err := b.bootstrap(testContext(), testBuilderOpts()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "terminated") + assert.True(t, f.podDeleted(), "a builder pod that died must be removed, not left for an operator") +} + +// The exec command is the whole transport: changing it to anything else breaks +// every build, and nothing in the required suite would notice if it were not +// asserted here. +func TestKubernetesBuilderDialsBuildctlDialStdio(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = true + + b := newTestBuilder(t, f, time.Minute) + + conn, err := b.dialerFor(testContext())(testContext(), "") + if err == nil { + t.Cleanup(func() { _ = conn.Close() }) + } + + assert.Eventually(t, func() bool { + for _, call := range f.recordedCalls() { + if strings.Contains(call, "command=buildctl") && strings.Contains(call, "command=dial-stdio") { + return true + } + } + + return false + }, 5*time.Second, 20*time.Millisecond, "the exec request must run buildctl dial-stdio in the builder container") +} + +// Remove on the happy path — a builder that came up and is torn down after a +// successful build — is what the required suite never reaches, so it is asserted +// here rather than left to the opt-in cluster job. +func TestBuilderRemoveDeletesTheProvisionedPod(t *testing.T) { + f := newFakeAPIServer(t) + f.readyPod = true + + kb := newTestBuilder(t, f, time.Minute) + require.NoError(t, kb.bootstrap(testContext(), testBuilderOpts())) + require.False(t, f.podDeleted()) + + builder := &Builder{builderName: kb.podName, kubernetesBuilder: kb, logger: smokeLogger{t}} + + require.NoError(t, builder.Remove(testContext())) + assert.True(t, f.podDeleted(), "removing the builder must delete the pod it provisioned") +} + +func TestBuildkitPodDefaults(t *testing.T) { + pod := buildkitPod("trdl-builder-42", testBuilderOpts()) + + require.Len(t, pod.Spec.Containers, 1) + container := pod.Spec.Containers[0] + + assert.Equal(t, corev1.RestartPolicyNever, pod.Spec.RestartPolicy) + assert.Equal(t, defaultBuildkitImage, container.Image) + assert.True(t, *container.SecurityContext.Privileged) + assert.Equal(t, []string{"buildctl", "debug", "workers"}, container.ReadinessProbe.Exec.Command) + assert.Nil(t, pod.Spec.ActiveDeadlineSeconds, "no deadline is set unless one is configured") + assert.Equal(t, map[string]string{"app": "trdl-builder-42"}, pod.Labels) +} + +func TestBuildkitPodRootless(t *testing.T) { + opts, err := parseKubernetesDriverOpts([]string{"rootless=true"}) + require.NoError(t, err) + + pod := buildkitPod("trdl-builder-42", opts) + container := pod.Spec.Containers[0] + + assert.Equal(t, defaultRootlessBuildkitImage, container.Image) + assert.Contains(t, container.Args, "--oci-worker-no-process-sandbox") + assert.Nil(t, container.SecurityContext.Privileged, "rootless must not ask for a privileged container") + assert.Equal(t, corev1.SeccompProfileTypeUnconfined, container.SecurityContext.SeccompProfile.Type) + // The annotation alone is not enough on a cluster that no longer converts it + // into the field, so both have to be present. + assert.Equal(t, corev1.AppArmorProfileTypeUnconfined, container.SecurityContext.AppArmorProfile.Type) + assert.Equal(t, "unconfined", pod.Annotations["container.apparmor.security.beta.kubernetes.io/buildkitd"]) + assert.Len(t, pod.Spec.Volumes, 1) + assert.Len(t, container.VolumeMounts, 1) +} + +func TestBuildkitPodRootlessKeepsConfiguredImage(t *testing.T) { + opts, err := parseKubernetesDriverOpts([]string{"rootless=true", "image=registry.example.com/buildkit:v0.31.2-rootless"}) + require.NoError(t, err) + + assert.Equal(t, "registry.example.com/buildkit:v0.31.2-rootless", buildkitPod("trdl-builder-42", opts).Spec.Containers[0].Image) +} + +func TestBuildkitPodAppliesResourcesAndScheduling(t *testing.T) { + opts, err := parseKubernetesDriverOpts([]string{ + "requests.cpu=500m", + "limits.memory=4Gi", + "nodeselector=disktype=ssd,zone=a", + "serviceaccount=trdl-buildkit", + "deadline=90m", + "labels=team=delivery", + "annotations=example.com/owner=trdl", + }) + require.NoError(t, err) + + pod := buildkitPod("trdl-builder-42", opts) + container := pod.Spec.Containers[0] + + assert.Equal(t, "500m", container.Resources.Requests.Cpu().String()) + assert.Equal(t, "4Gi", container.Resources.Limits.Memory().String()) + assert.Equal(t, map[string]string{"disktype": "ssd", "zone": "a"}, pod.Spec.NodeSelector) + assert.Equal(t, "trdl-buildkit", pod.Spec.ServiceAccountName) + assert.Equal(t, int64(5400), *pod.Spec.ActiveDeadlineSeconds) + assert.Equal(t, "delivery", pod.Labels["team"]) + assert.Equal(t, "trdl", pod.Annotations["example.com/owner"]) +} + +// One name=value pair per element is what the field's own description asks for, +// so the same option written twice is a natural shape — and replacing instead of +// merging would drop the earlier pairs with no error anywhere. +func TestParseKubernetesDriverOptsMergesRepeatedMapOptions(t *testing.T) { + opts, err := parseKubernetesDriverOpts([]string{ + "nodeselector=kubernetes.io/arch=amd64", + "nodeselector=workload=build", + "labels=team=delivery", + "labels=cost-center=42", + }) + + require.NoError(t, err) + assert.Equal(t, map[string]string{"kubernetes.io/arch": "amd64", "workload": "build"}, opts.nodeSelector) + assert.Equal(t, map[string]string{"team": "delivery", "cost-center": "42"}, opts.labels) +} + +// A blank image must mean "not set", or it silently suppresses the rootless +// default and stores a pod spec the API server refuses at release time. +func TestParseKubernetesDriverOptsBlankImageKeepsDefault(t *testing.T) { + opts, err := parseKubernetesDriverOpts([]string{"rootless=true", "image= "}) + + require.NoError(t, err) + assert.Equal(t, defaultRootlessBuildkitImage, opts.image) +} + +func TestParseKubernetesDriverOptsRejections(t *testing.T) { + for name, driverOpts := range map[string][]string{ + "unsupported option": {"tolerations=key=node,operator=Exists"}, + "not a pair": {"namespace"}, + "bad boolean": {"rootless=yes-please"}, + "bad duration": {"deadline=90"}, + "bad quantity": {"limits.memory=4 gigabytes"}, + "negative deadline": {"deadline=-1m"}, + "sub-second deadline": {"deadline=500ms"}, + "truncating deadline": {"deadline=1500ms"}, + "negative cpu request": {"requests.cpu=-1"}, + "negative memory limit": {"limits.memory=-500Mi"}, + "non-positive timeout": {"timeout=0s"}, + } { + rejected := driverOpts + t.Run(name, func(t *testing.T) { + _, err := parseKubernetesDriverOpts(rejected) + assert.Error(t, err) + }) + } +} + +func TestParseKubernetesDriverOptsTimeoutDefaults(t *testing.T) { + opts, err := parseKubernetesDriverOpts(nil) + + require.NoError(t, err) + assert.Equal(t, defaultBuildkitPodTimeout, opts.timeout) + + opts, err = parseKubernetesDriverOpts([]string{"timeout=10s"}) + + require.NoError(t, err) + assert.Equal(t, 10*time.Second, opts.timeout) +} + +func TestValidateBuildkitdDriverOpts(t *testing.T) { + assert.NoError(t, ValidateBuildkitdDriverOpts(context.Background(), "", nil)) + assert.NoError(t, ValidateBuildkitdDriverOpts(context.Background(), "", []string{" "})) + assert.Error(t, ValidateBuildkitdDriverOpts(context.Background(), "", []string{"namespace=trdl-build"}), + "options without a driver would never be applied") + assert.NoError(t, ValidateBuildkitdDriverOpts(context.Background(), "kubernetes", []string{"namespace=trdl-build"})) + assert.Error(t, ValidateBuildkitdDriverOpts(context.Background(), "kubernetes", []string{"replicas=3"})) +} + +func TestValidateBuildkitdDriver(t *testing.T) { + assert.NoError(t, ValidateBuildkitdDriver(context.Background(), "")) + assert.NoError(t, ValidateBuildkitdDriver(context.Background(), "kubernetes")) + assert.Error(t, ValidateBuildkitdDriver(context.Background(), "docker-container")) +}