From 714e68fab7ce6e32711c128a0e28f3d2f489b6e8 Mon Sep 17 00:00:00 2001 From: Emily Keefe Date: Thu, 16 Apr 2026 14:29:01 -0400 Subject: [PATCH 1/5] Create the rover-group-sync maintenance script This script is responsible for syncing the Konflux Rover groups from the internal Red HatLDAP server to the internal infrastructure Git repository's 'groups' directory. Generated-by: Cursor KFLUXINFRA-3597 --- maintenance/rover-group-sync/Dockerfile | 21 + maintenance/rover-group-sync/README.md | 48 ++ .../rover-group-sync/sync-rover-groups.sh | 114 +++++ .../test/fixtures/ldap-sync-config.yaml | 29 ++ .../test/stubs/find-and-delete-fail | 11 + .../rover-group-sync/test/stubs/sed-stub | 10 + .../rover-group-sync/test/stubs/stub-git | 37 ++ .../rover-group-sync/test/stubs/stub-oc | 74 +++ .../rover-group-sync/test/stubs/stub-sed | 10 + .../rover-group-sync/test/stubs/stub-yq | 40 ++ .../test/sync-rover-groups.bats | 468 ++++++++++++++++++ .../rover-group-sync/test/test_helpers.bash | 17 + 12 files changed, 879 insertions(+) create mode 100644 maintenance/rover-group-sync/Dockerfile create mode 100644 maintenance/rover-group-sync/README.md create mode 100644 maintenance/rover-group-sync/sync-rover-groups.sh create mode 100644 maintenance/rover-group-sync/test/fixtures/ldap-sync-config.yaml create mode 100755 maintenance/rover-group-sync/test/stubs/find-and-delete-fail create mode 100644 maintenance/rover-group-sync/test/stubs/sed-stub create mode 100755 maintenance/rover-group-sync/test/stubs/stub-git create mode 100755 maintenance/rover-group-sync/test/stubs/stub-oc create mode 100755 maintenance/rover-group-sync/test/stubs/stub-sed create mode 100755 maintenance/rover-group-sync/test/stubs/stub-yq create mode 100644 maintenance/rover-group-sync/test/sync-rover-groups.bats create mode 100644 maintenance/rover-group-sync/test/test_helpers.bash diff --git a/maintenance/rover-group-sync/Dockerfile b/maintenance/rover-group-sync/Dockerfile new file mode 100644 index 0000000..7a7f906 --- /dev/null +++ b/maintenance/rover-group-sync/Dockerfile @@ -0,0 +1,21 @@ +FROM quay.io/openshift/origin-cli:4.18 + +# Install yq (mikefarah v4): verify SHA256 of the release binary before install (supply-chain). +# Hash is for https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 (linux/amd64). +ARG YQ_VERSION=v4.47.1 +ARG YQ_SHA256=0fb28c6680193c41b364193d0c0fc4a03177aecde51cfc04d506b1517158c2fb + +RUN curl -fsSL -o /tmp/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \ + && echo "${YQ_SHA256} /tmp/yq" | sha256sum -c - \ + && mv /tmp/yq /usr/local/bin/yq \ + && chmod +x /usr/local/bin/yq + +# Copy the sync script +WORKDIR /scripts + +COPY sync-rover-groups.sh /scripts/sync-rover-groups.sh + +RUN chmod +x /scripts/sync-rover-groups.sh + +# Run the sync script +ENTRYPOINT ["/bin/bash", "/scripts/sync-rover-groups.sh"] diff --git a/maintenance/rover-group-sync/README.md b/maintenance/rover-group-sync/README.md new file mode 100644 index 0000000..0618406 --- /dev/null +++ b/maintenance/rover-group-sync/README.md @@ -0,0 +1,48 @@ +# rover-group-sync + +## `sync-rover-groups.sh` + +Bash script intended to run in a Kubernetes CronJob (see `Dockerfile`) It: + +1. **Validates** that `oc`, `yq` (mikefarah v4), and `git` are available, and that required paths and environment variables are present. +2. **Prepares LDAP sync config** by copying the LDAP sync template and injecting `LDAP_PASSWORD`, `LDAP_DN`, and the CA path with `yq` in-place edits. +3. **Clones** the target Git repository (branch from `GIT_BRANCH`, default `main`) into a work directory. +4. **Syncs OpenShift Groups from LDAP** with `oc adm groups sync`, normalizes the `List` output with `yq`, and writes **one YAML file per group** under `groups/`, using a filename derived from `metadata.name` (non-alphanumeric characters sanitized with `sed`). +5. **Commits and pushes** only if `groups/` changed; otherwise exits successfully without a commit. + +Typical inputs are mounted files (`SYNC_CONFIG_SOURCE`, `LDAP_CA_PATH`, `GIT_REPO_SSH_PATH`) and secrets (`GIT_REPO_URL`, `LDAP_DN`, `LDAP_PASSWORD`, `GIT_PUBLIC_SSH_KEY`). For Git over SSH, the script sets **`StrictHostKeyChecking=yes`** and writes a **temporary `known_hosts`** file whose line is `github.com` plus the key type and base64 key read from **`GIT_SSH_PUBLIC_KEY`**. Do not disable host key verification in production. + +The script supports overriding command paths (`OC`, `YQ`, `GIT`, `SED`, `FIND`, `DATE_CMD`, etc.) and `GIT_SSH_COMMAND` for testing or nonstandard installs. + +## Tests + +Tests are written with [BATS](https://github.com/bats-core/bats-core) and live under `test/`. They use shell stubs (`test/stubs/`) and fixtures (`test/fixtures/`) to simulate `oc`, `git`, `yq`, failures, and local Git remotes without a real cluster or network. + +**Requirements to run the suite:** + +- `bats` (bats-core) +- `yq` (mikefarah v4) +- `git` +- `sed` and `find` (for tests that exercise those code paths) + +Install BATS with your OS package manager (for example `dnf install bats` on Fedora) or install [bats-core from GitHub](https://github.com/bats-core/bats-core#installing-bats-from-source). + +**Run all tests** from the repository root: + +```bash +bats maintenance/rover-group-sync/test/sync-rover-groups.bats +``` + +Or from this directory: + +```bash +cd maintenance/rover-group-sync +bats test/sync-rover-groups.bats +``` + +**Stub environment variables** (used only in tests): + +- `CASE` — selects behavior for `stub-oc` (`single`, `multi`, `sanitize`, `sync-fail`, `malformed-yaml`, …). +- `REASON` — selects failure modes for `stub-git` and `stub-yq` (for example `clone`, `commit`, `push`, `password`, `dn`, `ca`, `metadata-name`). + +These are unset between tests in `setup()` so cases do not leak into each other. diff --git a/maintenance/rover-group-sync/sync-rover-groups.sh b/maintenance/rover-group-sync/sync-rover-groups.sh new file mode 100644 index 0000000..de7d714 --- /dev/null +++ b/maintenance/rover-group-sync/sync-rover-groups.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# Inject credentials into LDAP config, sync LDAP groups via oc adm groups sync, +# write one portable Group manifest per file, commit + push. +# +# Intended for use in a Kubernetes CronJob; requires oc, yq (mikefarah v4), git. + +set -euo pipefail + +# Environment overrides (useful for tests): OC, YQ, GIT, SED, FIND, DATE_CMD, +OC="${OC:-oc}" +YQ="${YQ:-yq}" +GIT="${GIT:-git}" +SED="${SED:-sed}" +FIND="${FIND:-find}" +DATE_CMD="${DATE_CMD:-date}" + +SYNC_CONFIG_SOURCE="${SYNC_CONFIG_SOURCE:-/config/ldap-sync-config.yaml}" +LDAP_CA_PATH="${LDAP_CA_PATH:-/secrets/ca.crt}" +GIT_PRIVATE_SSH_PATH="${GIT_PRIVATE_SSH_PATH:-/secrets/git-ssh/ssh_private}" +BRANCH="${GIT_BRANCH:-main}" +# Optional: export KUBECONFIG only if your oc build requires an apiserver even for LDAP-only sync. +# [[ -n "${KUBECONFIG:-}" ]] || export KUBECONFIG=/var/run/kubeconfig/kubeconfig + +# Check for requirements +if ! command -v "${OC}" >/dev/null || ! command -v "${YQ}" >/dev/null || ! command -v "${GIT}" >/dev/null; then + echo "missing oc, yq, or git in PATH" >&2 + exit 1 +fi + +# Check environment variable values +if [[ ! -f "${SYNC_CONFIG_SOURCE}" ]]; then + echo "missing LDAP sync config template: ${SYNC_CONFIG_SOURCE}" >&2 + exit 1 +fi +if [[ ! -f "${LDAP_CA_PATH}" ]]; then + echo "missing LDAP CA file: ${LDAP_CA_PATH}" >&2 + exit 1 +fi +if [[ ! -f "${GIT_PRIVATE_SSH_PATH}" ]]; then + echo "missing Git repo SSH private key: ${GIT_PRIVATE_SSH_PATH}" >&2 + exit 1 +fi + +for _required in GIT_REPO_URL LDAP_DN LDAP_PASSWORD GIT_SSH_PUBLIC_KEY; do + if [[ -z "${!_required:-}" ]]; then + echo "${_required} must be set to a non-empty string" >&2 + exit 1 + fi +done + +# Inject credentials into LDAP config writable copy; ConfigMap mount is read-only +SYNC_CONFIG_FILE="$(mktemp)" +trap 'rm -f "${SYNC_CONFIG_FILE}"' EXIT +cp "${SYNC_CONFIG_SOURCE}" "${SYNC_CONFIG_FILE}" + +export LDAP_PASSWORD LDAP_DN LDAP_CA_PATH +"${YQ}" -i '.bindPassword = strenv(LDAP_PASSWORD)' "${SYNC_CONFIG_FILE}" +"${YQ}" -i '.bindDN = strenv(LDAP_DN)' "${SYNC_CONFIG_FILE}" +"${YQ}" -i '.ca = strenv(LDAP_CA_PATH)' "${SYNC_CONFIG_FILE}" + +# Clone branch into work directory (tests may set WORKDIR to a fixed path) +WORKDIR="${WORKDIR:-$(mktemp -d)}" +rm -rf "${WORKDIR}" +mkdir -p "${WORKDIR}" + +KNOWN_HOSTS_PATH="$(mktemp)" +trap 'rm -f "${SYNC_CONFIG_FILE}" "${KNOWN_HOSTS_PATH}"' EXIT +printf 'github.com %s\n' "$(awk 'NF {print $1, $2; exit}' <<< "${GIT_SSH_PUBLIC_KEY}")" >"${KNOWN_HOSTS_PATH}" + + +if [[ -z "${GIT_SSH_COMMAND:-}" ]]; then + export GIT_SSH_COMMAND="ssh -i $(printf '%q' "${GIT_PRIVATE_SSH_PATH}") -o UserKnownHostsFile=$(printf '%q' "${KNOWN_HOSTS_PATH}") -o StrictHostKeyChecking=yes" +fi + +"${GIT}" clone --depth 1 --branch "${BRANCH}" "${GIT_REPO_URL}" "${WORKDIR}" +cd "${WORKDIR}" + +# Get all Group objects - portable only (no annotations/labels/cluster metadata) +LIST_TMP="$(mktemp)" +trap 'rm -f "${LIST_TMP}" "${SYNC_CONFIG_FILE}" "${KNOWN_HOSTS_PATH}"' EXIT + +echo "Retrieving groups from LDAP..." +"${OC}" adm groups sync --sync-config="${SYNC_CONFIG_FILE}" -o yaml | "${YQ}" \ + '.items |= map({"apiVersion": .apiVersion, "kind": .kind, "metadata": {"name": .metadata.name}, "users": (.users // [])})' \ + >"${LIST_TMP}" + +COUNT="$("${YQ}" '.items | length' "${LIST_TMP}")" + +# Create Group manifests in target directory (and sanitize the group name) +echo "Creating Group manifests in target directory..." +TARGET_DIR="${WORKDIR}/groups" +mkdir -p "${TARGET_DIR}" +"${FIND}" "${TARGET_DIR}" -maxdepth 1 -type f -name '*.yaml' -delete + +i=0 +while [[ "${i}" -lt "${COUNT}" ]]; do + name="$("${YQ}" ".items[${i}].metadata.name" "${LIST_TMP}")" + safe="$(printf '%s' "${name}" | "${SED}" 's/[^a-zA-Z0-9._-]/_/g')" + "${YQ}" ".items[${i}]" "${LIST_TMP}" -o yaml >"${TARGET_DIR}/${safe}.yaml" + i=$((i + 1)) +done + +# Commit to Git repo if the groups were updated +"${GIT}" add "groups" +if "${GIT}" diff --cached --quiet; then + echo "No group manifest changes; skipping commit." + exit 0 +fi + +"${GIT}" -c user.email="${GIT_AUTHOR_EMAIL:-group-sync@local}" -c user.name="${GIT_AUTHOR_NAME:-group-sync-bot}" \ + commit -m "chore(groups): sync rover LDAP groups ${BRANCH} $("${DATE_CMD}" -u +%Y-%m-%dT%H:%M:%SZ)" + +"${GIT}" push "${GIT_REPO_URL}" "${BRANCH}" diff --git a/maintenance/rover-group-sync/test/fixtures/ldap-sync-config.yaml b/maintenance/rover-group-sync/test/fixtures/ldap-sync-config.yaml new file mode 100644 index 0000000..32e4a34 --- /dev/null +++ b/maintenance/rover-group-sync/test/fixtures/ldap-sync-config.yaml @@ -0,0 +1,29 @@ +# Minimal LDAPSyncConfig for tests and local runs (same schema as production template). +kind: LDAPSyncConfig +apiVersion: v1 +url: ldaps://ldap.example.test +bindDN: "REPLACE_WITH_BIND_DN" +bindPassword: "REPLACE_WITH_BIND_PASSWORD" +insecure: false +ca: "REPLACE_WITH_CA_PATH" + +rfc2307: + groupsQuery: + baseDN: "ou=test,dc=example,dc=test" + derefAliases: never + scope: sub + filter: "(objectClass=groupOfNames)" + groupUIDAttribute: dn + groupNameAttributes: + - cn + groupMembershipAttributes: + - uniqueMember + usersQuery: + baseDN: "dc=example,dc=test" + derefAliases: never + scope: sub + userNameAttributes: + - uid + userUIDAttribute: dn + tolerateMemberNotFoundErrors: true + tolerateMemberOutOfScopeErrors: true diff --git a/maintenance/rover-group-sync/test/stubs/find-and-delete-fail b/maintenance/rover-group-sync/test/stubs/find-and-delete-fail new file mode 100755 index 0000000..9c6d7db --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/find-and-delete-fail @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Delegates to real find(1) except the rover-group-sync cleanup of groups/*.yaml. +REAL_FIND="$(command -v find)" +[[ -n "${REAL_FIND}" ]] || { echo "stub find: real find not on PATH" >&2; exit 99; } + +target="${1:-}" +if [[ "$(basename -- "${target}")" == "groups" ]] && [[ "$*" == *"-name"* ]] && [[ "$*" == *"-delete"* ]]; then + echo "find: simulated failure deleting existing group yaml files" >&2 + exit 1 +fi +exec "${REAL_FIND}" "$@" diff --git a/maintenance/rover-group-sync/test/stubs/sed-stub b/maintenance/rover-group-sync/test/stubs/sed-stub new file mode 100644 index 0000000..0364fb5 --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/sed-stub @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Delegates to real sed except the group filename sanitization used in sync-rover-groups.sh. +REAL_SED="$(command -v sed)" +[[ -n "${REAL_SED}" ]] || { echo "stub sed: real sed not on PATH" >&2; exit 99; } + +if [[ "${1:-}" == 's/[^a-zA-Z0-9._-]/_/g' ]]; then + echo "sed: simulated filename sanitize failure" >&2 + exit 1 +fi +exec "${REAL_SED}" "$@" diff --git a/maintenance/rover-group-sync/test/stubs/stub-git b/maintenance/rover-group-sync/test/stubs/stub-git new file mode 100755 index 0000000..a1e9eb6 --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/stub-git @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Wraps real git and only fails based on the REASON environment variable. +# clone — first arg is "clone" +# commit — argv contains the subcommand "commit" +# push — argv contains the subcommand "push" +REAL_GIT="$(command -v git)" +[[ -n "${REAL_GIT}" ]] || { echo "stub git: real git not on PATH" >&2; exit 99; } + +case "${REASON:-}" in + clone) + if [[ "${1:-}" == "clone" ]]; then + echo "git: simulated clone failure" >&2 + exit 1 + fi + ;; + commit) + for _arg in "$@"; do + if [[ "${_arg}" == "commit" ]]; then + echo "git: simulated commit failure" >&2 + exit 1 + fi + done + ;; + push) + for _arg in "$@"; do + if [[ "${_arg}" == "push" ]]; then + echo "git: simulated push failure" >&2 + exit 1 + fi + done + ;; + *) + echo "stub git: set REASON to clone, commit, or push" >&2 + exit 99 + ;; +esac +exec "${REAL_GIT}" "$@" diff --git a/maintenance/rover-group-sync/test/stubs/stub-oc b/maintenance/rover-group-sync/test/stubs/stub-oc new file mode 100755 index 0000000..7be7e23 --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/stub-oc @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Test double for `oc adm groups sync -o yaml`. Set CASE: +# single — one Group (test-group) +# multi — multiple Groups rover-alpha, rover-bravo +# sanitize — one Group with a name to be sanitized (konflux:weird/name +# sync-fail — sync exits 1 (LDAP/API error) +# malformed-yaml — stdout is not valid YAML (breaks the yq pipe) + +if [[ "${1:-}" != "adm" || "${2:-}" != "groups" || "${3:-}" != "sync" ]]; then + echo "stub oc: unsupported invocation: $*" >&2 + exit 99 +fi + +case "${CASE:-}" in + single) + cat <<'EOF' +apiVersion: v1 +kind: List +items: + - apiVersion: user.openshift.io/v1 + kind: Group + metadata: + name: test-group + users: + - user1 +EOF + exit 0 + ;; + multi) + cat <<'EOF' +apiVersion: v1 +kind: List +items: + - apiVersion: user.openshift.io/v1 + kind: Group + metadata: + name: rover-alpha + users: [] + - apiVersion: user.openshift.io/v1 + kind: Group + metadata: + name: rover-bravo + users: + - user-one +EOF + exit 0 + ;; + sanitize) + cat <<'EOF' +apiVersion: v1 +kind: List +items: + - apiVersion: user.openshift.io/v1 + kind: Group + metadata: + name: konflux:weird/name + users: + - sync-test-user +EOF + exit 0 + ;; + sync-fail) + echo "oc: simulated adm groups sync failure" >&2 + exit 1 + ;; + malformed-yaml) + printf '%s\n' 'INVALID_YAML: [unclosed' + exit 0 + ;; + *) + echo "stub oc: set CASE to single, multi, sanitize, sync-fail, or malformed-yaml" >&2 + exit 99 + ;; +esac diff --git a/maintenance/rover-group-sync/test/stubs/stub-sed b/maintenance/rover-group-sync/test/stubs/stub-sed new file mode 100755 index 0000000..0364fb5 --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/stub-sed @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Delegates to real sed except the group filename sanitization used in sync-rover-groups.sh. +REAL_SED="$(command -v sed)" +[[ -n "${REAL_SED}" ]] || { echo "stub sed: real sed not on PATH" >&2; exit 99; } + +if [[ "${1:-}" == 's/[^a-zA-Z0-9._-]/_/g' ]]; then + echo "sed: simulated filename sanitize failure" >&2 + exit 1 +fi +exec "${REAL_SED}" "$@" diff --git a/maintenance/rover-group-sync/test/stubs/stub-yq b/maintenance/rover-group-sync/test/stubs/stub-yq new file mode 100755 index 0000000..67c9756 --- /dev/null +++ b/maintenance/rover-group-sync/test/stubs/stub-yq @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Wraps real yq and only fails based on the REASON environment variable. +# password — LDAP template inject @ .bindPassword cannot find that path +# dn — LDAP template inject @ .bindDN cannot find that path +# ca — LDAP template inject @ .ca cannot find that path +# metadata-name — loop read .items[i].metadata.name cannot find that path +REAL_YQ="$(command -v yq)" +[[ -n "${REAL_YQ}" ]] || { echo "stub yq: real yq not on PATH" >&2; exit 99; } + +case "${REASON:-}" in + password) + if [[ "$*" == *bindPassword* ]]; then + echo "yq: cannot find .bindPassword" >&2 + exit 1 + fi + ;; + dn) + if [[ "$*" == *bindDN* ]]; then + echo "yq: cannot find .bindDN" >&2 + exit 1 + fi + ;; + ca) + if [[ "$*" == *".ca = strenv"* ]]; then + echo "yq: cannot find .ca" >&2 + exit 1 + fi + ;; + metadata-name) + if [[ "${1:-}" =~ ^\.items\[[0-9]+\]\.metadata\.name$ ]]; then + echo "yq: cannot find .items[i].metadata.name" >&2 + exit 1 + fi + ;; + *) + echo "stub yq: set REASON to password, dn, ca, or metadata-name" >&2 + exit 99 + ;; +esac +exec "${REAL_YQ}" "$@" diff --git a/maintenance/rover-group-sync/test/sync-rover-groups.bats b/maintenance/rover-group-sync/test/sync-rover-groups.bats new file mode 100644 index 0000000..8c1c473 --- /dev/null +++ b/maintenance/rover-group-sync/test/sync-rover-groups.bats @@ -0,0 +1,468 @@ +#!/usr/bin/env bats +# +# Requires: bats, yq (mikefarah v4), git — for tests that exercise real tools. +# Run: bats maintenance/rover-group-sync/test/sync-rover-groups.bats + +load test_helpers + +setup() { + test_root="$(mktemp -d)" + export SYNC_CONFIG_SOURCE="${test_root}/ldap-sync-config.yaml" + export LDAP_CA_PATH="${test_root}/ca.crt" + export GIT_PRIVATE_SSH_PATH="${test_root}/ssh_key" + cp "${BATS_TEST_DIRNAME}/fixtures/ldap-sync-config.yaml" "${SYNC_CONFIG_SOURCE}" + : >"${LDAP_CA_PATH}" + : >"${GIT_PRIVATE_SSH_PATH}" + export GIT_SSH_PUBLIC_KEY="${test_root}/ssh_public" + # OpenSSH .pub-style line (type + key + comment); script builds known_hosts as github.com + $1 $2 + echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl test-key" >"${GIT_SSH_PUBLIC_KEY}" + export LDAP_DN="cn=test-bind,dc=example,dc=test" + export LDAP_PASSWORD="test-password" + export WORKDIR="${test_root}/workdir" + export GIT_BRANCH="${GIT_BRANCH:-main}" + SCRIPT="${BATS_TEST_DIRNAME}/../sync-rover-groups.sh" + unset REASON + unset CASE +} + +stub_binaries() { + export OC="/bin/true" + export YQ="/bin/true" + export GIT="/bin/true" +} + +# --- missing binaries (command -v) --- + +@test "fails when yq is not installed (YQ points to missing file)" { + stub_binaries + export YQ="${test_root}/no-such-yq" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing oc, yq, or git"* ]] +} + +@test "fails when git is not installed (GIT points to missing file)" { + stub_binaries + export GIT="${test_root}/no-such-git" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing oc, yq, or git"* ]] +} + +@test "fails when oc is not installed (OC points to missing file)" { + stub_binaries + export OC="${test_root}/no-such-oc" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing oc, yq, or git"* ]] +} + +# --- empty environment variables --- + +@test "fails when SYNC_CONFIG_SOURCE is empty" { + stub_binaries + export SYNC_CONFIG_SOURCE="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing LDAP sync config"* ]] +} + +@test "fails when LDAP_CA_PATH is empty" { + stub_binaries + export LDAP_CA_PATH="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing LDAP CA file"* ]] +} + +@test "fails when GIT_PRIVATE_SSH_PATH is empty" { + stub_binaries + export GIT_PRIVATE_SSH_PATH="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing Git repo SSH private key"* ]] +} + +@test "fails when GIT_REPO_URL is empty" { + stub_binaries + export GIT_REPO_URL="" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"GIT_REPO_URL must be set"* ]] +} + +@test "fails when LDAP_DN is empty" { + stub_binaries + export LDAP_DN="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"LDAP_DN must be set"* ]] +} + +@test "fails when LDAP_PASSWORD is empty" { + stub_binaries + export LDAP_PASSWORD="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"LDAP_PASSWORD must be set"* ]] +} + +@test "fails when GIT_SSH_PUBLIC_KEY is empty" { + stub_binaries + export GIT_SSH_PUBLIC_KEY="" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"GIT_SSH_PUBLIC_KEY must be set"* ]] +} + +# --- missing files --- + +@test "fails when SYNC_CONFIG_SOURCE file does not exist" { + stub_binaries + export SYNC_CONFIG_SOURCE="${test_root}/no-such-config.yaml" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing LDAP sync config"* ]] +} + +@test "fails when LDAP_CA_PATH file does not exist" { + stub_binaries + export LDAP_CA_PATH="${test_root}/missing-ca.crt" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing LDAP CA file"* ]] +} + +@test "fails when GIT_PRIVATE_SSH_PATH file does not exist" { + stub_binaries + export GIT_PRIVATE_SSH_PATH="${test_root}/missing-ssh-key" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"missing Git repo SSH private key"* ]] +} + +# --- LDAP template injection (yq) failures --- + +@test "fails when injecting LDAP_PASSWORD (yq) fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + stub_binaries + export REASON=password + export YQ="${BATS_TEST_DIRNAME}/stubs/stub-yq" + chmod +x "${YQ}" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"cannot find .bindPassword"* ]] +} + +@test "fails when injecting LDAP_DN (yq) fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + stub_binaries + export REASON=dn + export YQ="${BATS_TEST_DIRNAME}/stubs/stub-yq" + chmod +x "${YQ}" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"cannot find .bindDN"* ]] +} + +@test "fails when injecting LDAP_CA_PATH (yq) fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + stub_binaries + export REASON=ca + export YQ="${BATS_TEST_DIRNAME}/stubs/stub-yq" + chmod +x "${YQ}" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"cannot find .ca"* ]] +} + +# --- git failures --- + +@test "fails when git clone fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + export OC="/bin/true" + export YQ="$(command -v yq)" + export REASON=clone + export GIT="${BATS_TEST_DIRNAME}/stubs/stub-git" + chmod +x "${GIT}" + export GIT_REPO_URL="https://example.invalid/repo.git" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated clone failure"* ]] +} + +@test "fails when git commit fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export REASON=commit + export GIT="${BATS_TEST_DIRNAME}/stubs/stub-git" + chmod +x "${GIT}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated commit failure"* ]] +} + +@test "fails when git push fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export REASON=push + export GIT="${BATS_TEST_DIRNAME}/stubs/stub-git" + chmod +x "${GIT}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated push failure"* ]] +} + +# --- oc adm groups sync failure --- + +@test "fails when oc adm groups sync fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=sync-fail + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export GIT="$(command -v git)" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated adm groups sync failure"* ]] +} + +# --- existing group files deletion failure + +@test "fails when deleting existing group yaml files under groups/ fails" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + [[ -n "$(command -v find)" ]] || skip "find not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export GIT="$(command -v git)" + export FIND="${BATS_TEST_DIRNAME}/stubs/find-and-delete-fail" + chmod +x "${FIND}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated failure deleting existing group yaml files"* ]] +} + +# --- manifest creation failures --- + +@test "fails when yq cannot parse oc group list output (malformed yaml)" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=malformed-yaml + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export GIT="$(command -v git)" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"Error:"* ]] + [[ "${output}" == *"yaml:"* ]] +} + +@test "fails when yq cannot read .items[i].metadata.name" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export REASON=metadata-name + export YQ="${BATS_TEST_DIRNAME}/stubs/stub-yq" + chmod +x "${YQ}" + export GIT="$(command -v git)" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"cannot find .items[i].metadata.name"* ]] +} + +@test "fails when sed cannot sanitize the group name for the filename" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + [[ -n "$(command -v sed)" ]] || skip "sed not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export YQ="$(command -v yq)" + export GIT="$(command -v git)" + export SED="${BATS_TEST_DIRNAME}/stubs/stub-sed" + chmod +x "${SED}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"simulated filename sanitize failure"* ]] +} + +# --- success paths --- + +@test "syncs groups, writes manifests, commits and pushes with one group" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + [[ -f "${WORKDIR}/groups/test-group.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/test-group.yaml" + [[ "${output}" == "test-group" ]] + + run git -C "${bare}" log --oneline -1 + [[ "${output}" == *"chore(groups): sync rover LDAP groups"* ]] +} + +@test "syncs groups, writes manifests, commits and pushes with multiple groups" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=multi + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + [[ -f "${WORKDIR}/groups/rover-alpha.yaml" ]] + [[ -f "${WORKDIR}/groups/rover-bravo.yaml" ]] + + run yq '.metadata.name' "${WORKDIR}/groups/rover-alpha.yaml" + [[ "${output}" == "rover-alpha" ]] + run yq '.users | length' "${WORKDIR}/groups/rover-alpha.yaml" + [[ "${output}" == "0" ]] + + run yq '.metadata.name' "${WORKDIR}/groups/rover-bravo.yaml" + [[ "${output}" == "rover-bravo" ]] + run yq '.users[0]' "${WORKDIR}/groups/rover-bravo.yaml" + [[ "${output}" == "user-one" ]] +} + +@test "syncs groups, sanitizes metadata.name into a safe filename (sed), writes manifests, commits and pushes with one group" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=sanitize + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + # konflux:weird/name -> colon and slash become underscores (see sync-rover-groups.sh sed) + [[ -f "${WORKDIR}/groups/konflux_weird_name.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/konflux_weird_name.yaml" + [[ "${output}" == "konflux:weird/name" ]] +} + +@test "exits 0 without commit when manifests are unchanged" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + [[ "${output}" == *"No group manifest changes"* ]] +} diff --git a/maintenance/rover-group-sync/test/test_helpers.bash b/maintenance/rover-group-sync/test/test_helpers.bash new file mode 100644 index 0000000..7ae5647 --- /dev/null +++ b/maintenance/rover-group-sync/test/test_helpers.bash @@ -0,0 +1,17 @@ +# shellcheck shell=bash +# Sourced by sync-rover-groups.bats + +init_bare_repo_with_empty_commit() { + local bare="$1" + git init --bare -b main "${bare}" + local tmp + tmp="$(mktemp -d)" + ( + cd "${tmp}" || exit 1 + git init -b main + git commit --allow-empty -m "init" + git remote add origin "file://${bare}" + git push -u origin main + ) + rm -rf "${tmp}" +} From f9f984ff204cf6e6a7d7001c22d7589791605588 Mon Sep 17 00:00:00 2001 From: red-hat-konflux Date: Thu, 23 Apr 2026 19:48:45 +0000 Subject: [PATCH 2/5] Red Hat Konflux update rover-group-sync Signed-off-by: red-hat-konflux --- .tekton/rover-group-sync-pull-request.yaml | 521 +++++++++++++++++++++ .tekton/rover-group-sync-push.yaml | 518 ++++++++++++++++++++ 2 files changed, 1039 insertions(+) create mode 100644 .tekton/rover-group-sync-pull-request.yaml create mode 100644 .tekton/rover-group-sync-push.yaml diff --git a/.tekton/rover-group-sync-pull-request.yaml b/.tekton/rover-group-sync-pull-request.yaml new file mode 100644 index 0000000..e005a8a --- /dev/null +++ b/.tekton/rover-group-sync-pull-request.yaml @@ -0,0 +1,521 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/redhat-appstudio/infrastructure?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/pull_request_number: '{{pull_request_number}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "true" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request" && target_branch + == "main" && ( "maintenance/rover-group-sync/***".pathChanged() || ".tekton/rover-group-sync-pull-request.yaml".pathChanged() + ) + creationTimestamp: null + labels: + appstudio.openshift.io/application: rover-group-sync + appstudio.openshift.io/component: rover-group-sync + pipelines.appstudio.openshift.io/type: build + name: rover-group-sync-on-pull-request + namespace: konflux-infra-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/redhat-user-workloads/konflux-infra-tenant/rover-group-sync:on-pr-{{revision}} + - name: image-expires-after + value: 5d + - name: dockerfile + value: Dockerfile + - name: path-context + value: maintenance/rover-group-sync + pipelineSpec: + description: | + This pipeline is ideal for building container images from a Containerfile while maintaining trust after pipeline customization. + + _Uses `buildah` to create a container image leveraging [trusted artifacts](https://konflux-ci.dev/architecture/ADR/0036-trusted-artifacts.html). It also optionally creates a source image and runs some build-time tests. Information is shared between tasks using OCI artifacts instead of PVCs. EC will pass the [`trusted_task.trusted`](https://conforma.dev/docs/policy/packages/release_trusted_task.html#trusted_task__trusted) policy as long as all data used to build the artifact is generated from trusted tasks. + This pipeline is pushed as a Tekton bundle to [quay.io](https://quay.io/repository/konflux-ci/tekton-catalog/pipeline-docker-build-oci-ta?tab=tags)_ + params: + - description: Source Repository URL + name: git-url + type: string + - default: "" + description: Revision of the Source Repository + name: revision + type: string + - description: Fully Qualified Output Image + name: output-image + type: string + - default: . + description: Path to the source code of an application's component from where + to build image. + name: path-context + type: string + - default: Dockerfile + description: Path to the Dockerfile inside the context specified by parameter + path-context + name: dockerfile + type: string + - default: "false" + description: Skip checks against built image + name: skip-checks + type: string + - default: "false" + description: Execute the build with network isolation + name: hermetic + type: string + - default: "" + description: Build dependencies to be prefetched + name: prefetch-input + type: string + - default: "" + description: Image tag expiration time, time values could be something like + 1h, 2d, 3w for hours, days, and weeks, respectively. + name: image-expires-after + type: string + - default: "false" + description: Build a source image. + name: build-source-image + type: string + - default: "false" + description: Add built image into an OCI image index + name: build-image-index + type: string + - default: docker + description: The format for the resulting image's mediaType. Valid values are + oci or docker. + name: buildah-format + type: string + - default: "false" + description: Enable cache proxy configuration + name: enable-cache-proxy + - default: "true" + description: Use the package registry proxy when prefetching dependencies + name: enable-package-registry-proxy + - default: [] + description: Array of --build-arg values ("arg=value" strings) for buildah + name: build-args + type: array + - default: "" + description: Path to a file with build arguments for buildah, see https://www.mankier.com/1/buildah-build#--build-arg-file + name: build-args-file + type: string + - default: "false" + description: Whether to enable privileged mode, should be used only with remote + VMs + name: privileged-nested + type: string + results: + - description: "" + name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - description: "" + name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - description: "" + name: CHAINS-GIT_URL + value: $(tasks.clone-repository.results.url) + - description: "" + name: CHAINS-GIT_COMMIT + value: $(tasks.clone-repository.results.commit) + tasks: + - name: init + params: + - name: enable-cache-proxy + value: $(params.enable-cache-proxy) + taskRef: + params: + - name: name + value: init + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4@sha256:b797dd453ddad669365de6de4649e3a9e37e77aa26eb9862ca079a36cbfe64a4 + - name: kind + value: task + resolver: bundles + - name: clone-repository + params: + - name: url + value: $(params.git-url) + - name: revision + value: $(params.revision) + - name: ociStorage + value: $(params.output-image).git + - name: ociArtifactExpiresAfter + value: $(params.image-expires-after) + runAfter: + - init + taskRef: + params: + - name: name + value: git-clone-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.1@sha256:13d49df7dc9ae301627e45f95a236011422996152f1bea46cd60217b0f057407 + - name: kind + value: task + resolver: bundles + workspaces: + - name: basic-auth + workspace: git-auth + - name: prefetch-dependencies + params: + - name: input + value: $(params.prefetch-input) + - name: enable-package-registry-proxy + value: $(params.enable-package-registry-proxy) + - name: SOURCE_ARTIFACT + value: $(tasks.clone-repository.results.SOURCE_ARTIFACT) + - name: ociStorage + value: $(params.output-image).prefetch + - name: ociArtifactExpiresAfter + value: $(params.image-expires-after) + runAfter: + - clone-repository + taskRef: + params: + - name: name + value: prefetch-dependencies-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.3@sha256:5aa530bb95fa77477dad200e4d5d8312ad8bfafef549957f19aa56f1428672fa + - name: kind + value: task + resolver: bundles + workspaces: + - name: git-basic-auth + workspace: git-auth + - name: netrc + workspace: netrc + - name: build-container + params: + - name: IMAGE + value: $(params.output-image) + - name: DOCKERFILE + value: $(params.dockerfile) + - name: CONTEXT + value: $(params.path-context) + - name: HERMETIC + value: $(params.hermetic) + - name: PREFETCH_INPUT + value: $(params.prefetch-input) + - name: IMAGE_EXPIRES_AFTER + value: $(params.image-expires-after) + - name: COMMIT_SHA + value: $(tasks.clone-repository.results.commit) + - name: BUILD_ARGS + value: + - $(params.build-args[*]) + - name: BUILD_ARGS_FILE + value: $(params.build-args-file) + - name: PRIVILEGED_NESTED + value: $(params.privileged-nested) + - name: SOURCE_URL + value: $(tasks.clone-repository.results.url) + - name: BUILDAH_FORMAT + value: $(params.buildah-format) + - name: HTTP_PROXY + value: $(tasks.init.results.http-proxy) + - name: NO_PROXY + value: $(tasks.init.results.no-proxy) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - prefetch-dependencies + taskRef: + params: + - name: name + value: buildah-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.9@sha256:681d9f65a7f50cb260ee576ccab551e11d63c549f1e1ef3d201da3c112855bd6 + - name: kind + value: task + resolver: bundles + - name: build-image-index + params: + - name: IMAGE + value: $(params.output-image) + - name: ALWAYS_BUILD_INDEX + value: $(params.build-image-index) + - name: IMAGES + value: + - $(tasks.build-container.results.IMAGE_URL)@$(tasks.build-container.results.IMAGE_DIGEST) + - name: BUILDAH_FORMAT + value: $(params.buildah-format) + runAfter: + - build-container + taskRef: + params: + - name: name + value: build-image-index + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3@sha256:550afde50349e22ec11191ea0db9a49395ab46fef4e8317d820b6e946677ebeb + - name: kind + value: task + resolver: bundles + - name: build-source-image + params: + - name: BINARY_IMAGE + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: BINARY_IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: source-build-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3@sha256:0917cfc7772e82cb8e74743c2104f43bcf2596aceafe87eec6fce69a8cac5f06 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.build-source-image) + operator: in + values: + - "true" + - name: deprecated-base-image-check + params: + - name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: deprecated-image-check + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:3457a4ca93f8d55f14ebd407532b1223c689eacc34f0abb3003db4111667bdae + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: clair-scan + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: clair-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.3@sha256:9397d3eb9f1cbebaa15e93256e0ca9eaca148baa674be72f07f4a00df63c4609 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: ecosystem-cert-preflight-checks + params: + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: ecosystem-cert-preflight-checks + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:2468c01818fbaad2235e4fca438f28e847260e3e354cf5a441bbd671684af2db + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-snyk-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-snyk-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.4@sha256:6045ed6f2d37cfdf75cb3f2bf88706839c276a59f892ae027a315456c2914cf3 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: clamav-scan + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: clamav-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:9f18b216ce71a66909e7cb17d9b34526c02d73cf12884ba32d1f10614f7b9f5a + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-shell-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-shell-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:c314b4d5369d7961af51c865be28cd792d5f233aef94ecf035b3f84acde398bf + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-unicode-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-unicode-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:3d8a6902ab7c5c2125be07263f395426342c5032b3abfd0140162ad838437bab + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: apply-tags + params: + - name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: apply-tags + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3@sha256:a291081de7fb27f832c6fc3c4b078acf7e6162ca4c085db38b118ca87e8b5b66 + - name: kind + value: task + resolver: bundles + - name: push-dockerfile + params: + - name: IMAGE + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: DOCKERFILE + value: $(params.dockerfile) + - name: CONTEXT + value: $(params.path-context) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: push-dockerfile-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3@sha256:7855471abfe87de080b914f2f3ca27c59e64f6448a7c2435e51435b764494c71 + - name: kind + value: task + resolver: bundles + - name: rpms-signature-scan + params: + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: rpms-signature-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:b2224a0442ac705e20a25b8609e1760321d9d86da7901fd0392a90102688e37d + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + workspaces: + - name: git-auth + optional: true + - name: netrc + optional: true + taskRunTemplate: + serviceAccountName: build-pipeline-rover-group-sync + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} diff --git a/.tekton/rover-group-sync-push.yaml b/.tekton/rover-group-sync-push.yaml new file mode 100644 index 0000000..7ddc554 --- /dev/null +++ b/.tekton/rover-group-sync-push.yaml @@ -0,0 +1,518 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + annotations: + build.appstudio.openshift.io/repo: https://github.com/redhat-appstudio/infrastructure?rev={{revision}} + build.appstudio.redhat.com/commit_sha: '{{revision}}' + build.appstudio.redhat.com/target_branch: '{{target_branch}}' + pipelinesascode.tekton.dev/cancel-in-progress: "false" + pipelinesascode.tekton.dev/max-keep-runs: "3" + pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch + == "main" && ( "maintenance/rover-group-sync/***".pathChanged() || ".tekton/rover-group-sync-push.yaml".pathChanged() + ) + creationTimestamp: null + labels: + appstudio.openshift.io/application: rover-group-sync + appstudio.openshift.io/component: rover-group-sync + pipelines.appstudio.openshift.io/type: build + name: rover-group-sync-on-push + namespace: konflux-infra-tenant +spec: + params: + - name: git-url + value: '{{source_url}}' + - name: revision + value: '{{revision}}' + - name: output-image + value: quay.io/redhat-user-workloads/konflux-infra-tenant/rover-group-sync:{{revision}} + - name: dockerfile + value: Dockerfile + - name: path-context + value: maintenance/rover-group-sync + pipelineSpec: + description: | + This pipeline is ideal for building container images from a Containerfile while maintaining trust after pipeline customization. + + _Uses `buildah` to create a container image leveraging [trusted artifacts](https://konflux-ci.dev/architecture/ADR/0036-trusted-artifacts.html). It also optionally creates a source image and runs some build-time tests. Information is shared between tasks using OCI artifacts instead of PVCs. EC will pass the [`trusted_task.trusted`](https://conforma.dev/docs/policy/packages/release_trusted_task.html#trusted_task__trusted) policy as long as all data used to build the artifact is generated from trusted tasks. + This pipeline is pushed as a Tekton bundle to [quay.io](https://quay.io/repository/konflux-ci/tekton-catalog/pipeline-docker-build-oci-ta?tab=tags)_ + params: + - description: Source Repository URL + name: git-url + type: string + - default: "" + description: Revision of the Source Repository + name: revision + type: string + - description: Fully Qualified Output Image + name: output-image + type: string + - default: . + description: Path to the source code of an application's component from where + to build image. + name: path-context + type: string + - default: Dockerfile + description: Path to the Dockerfile inside the context specified by parameter + path-context + name: dockerfile + type: string + - default: "false" + description: Skip checks against built image + name: skip-checks + type: string + - default: "false" + description: Execute the build with network isolation + name: hermetic + type: string + - default: "" + description: Build dependencies to be prefetched + name: prefetch-input + type: string + - default: "" + description: Image tag expiration time, time values could be something like + 1h, 2d, 3w for hours, days, and weeks, respectively. + name: image-expires-after + type: string + - default: "false" + description: Build a source image. + name: build-source-image + type: string + - default: "false" + description: Add built image into an OCI image index + name: build-image-index + type: string + - default: docker + description: The format for the resulting image's mediaType. Valid values are + oci or docker. + name: buildah-format + type: string + - default: "false" + description: Enable cache proxy configuration + name: enable-cache-proxy + - default: "true" + description: Use the package registry proxy when prefetching dependencies + name: enable-package-registry-proxy + - default: [] + description: Array of --build-arg values ("arg=value" strings) for buildah + name: build-args + type: array + - default: "" + description: Path to a file with build arguments for buildah, see https://www.mankier.com/1/buildah-build#--build-arg-file + name: build-args-file + type: string + - default: "false" + description: Whether to enable privileged mode, should be used only with remote + VMs + name: privileged-nested + type: string + results: + - description: "" + name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - description: "" + name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - description: "" + name: CHAINS-GIT_URL + value: $(tasks.clone-repository.results.url) + - description: "" + name: CHAINS-GIT_COMMIT + value: $(tasks.clone-repository.results.commit) + tasks: + - name: init + params: + - name: enable-cache-proxy + value: $(params.enable-cache-proxy) + taskRef: + params: + - name: name + value: init + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-init:0.4@sha256:b797dd453ddad669365de6de4649e3a9e37e77aa26eb9862ca079a36cbfe64a4 + - name: kind + value: task + resolver: bundles + - name: clone-repository + params: + - name: url + value: $(params.git-url) + - name: revision + value: $(params.revision) + - name: ociStorage + value: $(params.output-image).git + - name: ociArtifactExpiresAfter + value: $(params.image-expires-after) + runAfter: + - init + taskRef: + params: + - name: name + value: git-clone-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-git-clone-oci-ta:0.1@sha256:13d49df7dc9ae301627e45f95a236011422996152f1bea46cd60217b0f057407 + - name: kind + value: task + resolver: bundles + workspaces: + - name: basic-auth + workspace: git-auth + - name: prefetch-dependencies + params: + - name: input + value: $(params.prefetch-input) + - name: enable-package-registry-proxy + value: $(params.enable-package-registry-proxy) + - name: SOURCE_ARTIFACT + value: $(tasks.clone-repository.results.SOURCE_ARTIFACT) + - name: ociStorage + value: $(params.output-image).prefetch + - name: ociArtifactExpiresAfter + value: $(params.image-expires-after) + runAfter: + - clone-repository + taskRef: + params: + - name: name + value: prefetch-dependencies-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies-oci-ta:0.3@sha256:5aa530bb95fa77477dad200e4d5d8312ad8bfafef549957f19aa56f1428672fa + - name: kind + value: task + resolver: bundles + workspaces: + - name: git-basic-auth + workspace: git-auth + - name: netrc + workspace: netrc + - name: build-container + params: + - name: IMAGE + value: $(params.output-image) + - name: DOCKERFILE + value: $(params.dockerfile) + - name: CONTEXT + value: $(params.path-context) + - name: HERMETIC + value: $(params.hermetic) + - name: PREFETCH_INPUT + value: $(params.prefetch-input) + - name: IMAGE_EXPIRES_AFTER + value: $(params.image-expires-after) + - name: COMMIT_SHA + value: $(tasks.clone-repository.results.commit) + - name: BUILD_ARGS + value: + - $(params.build-args[*]) + - name: BUILD_ARGS_FILE + value: $(params.build-args-file) + - name: PRIVILEGED_NESTED + value: $(params.privileged-nested) + - name: SOURCE_URL + value: $(tasks.clone-repository.results.url) + - name: BUILDAH_FORMAT + value: $(params.buildah-format) + - name: HTTP_PROXY + value: $(tasks.init.results.http-proxy) + - name: NO_PROXY + value: $(tasks.init.results.no-proxy) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - prefetch-dependencies + taskRef: + params: + - name: name + value: buildah-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.9@sha256:681d9f65a7f50cb260ee576ccab551e11d63c549f1e1ef3d201da3c112855bd6 + - name: kind + value: task + resolver: bundles + - name: build-image-index + params: + - name: IMAGE + value: $(params.output-image) + - name: ALWAYS_BUILD_INDEX + value: $(params.build-image-index) + - name: IMAGES + value: + - $(tasks.build-container.results.IMAGE_URL)@$(tasks.build-container.results.IMAGE_DIGEST) + - name: BUILDAH_FORMAT + value: $(params.buildah-format) + runAfter: + - build-container + taskRef: + params: + - name: name + value: build-image-index + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.3@sha256:550afde50349e22ec11191ea0db9a49395ab46fef4e8317d820b6e946677ebeb + - name: kind + value: task + resolver: bundles + - name: build-source-image + params: + - name: BINARY_IMAGE + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: BINARY_IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: source-build-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-source-build-oci-ta:0.3@sha256:0917cfc7772e82cb8e74743c2104f43bcf2596aceafe87eec6fce69a8cac5f06 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.build-source-image) + operator: in + values: + - "true" + - name: deprecated-base-image-check + params: + - name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: deprecated-image-check + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:3457a4ca93f8d55f14ebd407532b1223c689eacc34f0abb3003db4111667bdae + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: clair-scan + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: clair-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.3@sha256:9397d3eb9f1cbebaa15e93256e0ca9eaca148baa674be72f07f4a00df63c4609 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: ecosystem-cert-preflight-checks + params: + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: ecosystem-cert-preflight-checks + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:2468c01818fbaad2235e4fca438f28e847260e3e354cf5a441bbd671684af2db + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-snyk-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-snyk-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.4@sha256:6045ed6f2d37cfdf75cb3f2bf88706839c276a59f892ae027a315456c2914cf3 + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: clamav-scan + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: clamav-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:9f18b216ce71a66909e7cb17d9b34526c02d73cf12884ba32d1f10614f7b9f5a + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-shell-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-shell-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check-oci-ta:0.1@sha256:c314b4d5369d7961af51c865be28cd792d5f233aef94ecf035b3f84acde398bf + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: sast-unicode-check + params: + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + - name: CACHI2_ARTIFACT + value: $(tasks.prefetch-dependencies.results.CACHI2_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: sast-unicode-check-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check-oci-ta:0.4@sha256:3d8a6902ab7c5c2125be07263f395426342c5032b3abfd0140162ad838437bab + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + - name: apply-tags + params: + - name: IMAGE_URL + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: apply-tags + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.3@sha256:a291081de7fb27f832c6fc3c4b078acf7e6162ca4c085db38b118ca87e8b5b66 + - name: kind + value: task + resolver: bundles + - name: push-dockerfile + params: + - name: IMAGE + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: IMAGE_DIGEST + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + - name: DOCKERFILE + value: $(params.dockerfile) + - name: CONTEXT + value: $(params.path-context) + - name: SOURCE_ARTIFACT + value: $(tasks.prefetch-dependencies.results.SOURCE_ARTIFACT) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: push-dockerfile-oci-ta + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile-oci-ta:0.3@sha256:7855471abfe87de080b914f2f3ca27c59e64f6448a7c2435e51435b764494c71 + - name: kind + value: task + resolver: bundles + - name: rpms-signature-scan + params: + - name: image-url + value: $(tasks.build-image-index.results.IMAGE_URL) + - name: image-digest + value: $(tasks.build-image-index.results.IMAGE_DIGEST) + runAfter: + - build-image-index + taskRef: + params: + - name: name + value: rpms-signature-scan + - name: bundle + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:b2224a0442ac705e20a25b8609e1760321d9d86da7901fd0392a90102688e37d + - name: kind + value: task + resolver: bundles + when: + - input: $(params.skip-checks) + operator: in + values: + - "false" + workspaces: + - name: git-auth + optional: true + - name: netrc + optional: true + taskRunTemplate: + serviceAccountName: build-pipeline-rover-group-sync + workspaces: + - name: git-auth + secret: + secretName: '{{ git_auth_secret }}' +status: {} From 316a2c3133dfa60edff8d770cd32bbd24950d0ec Mon Sep 17 00:00:00 2001 From: Emily Keefe Date: Thu, 23 Apr 2026 17:15:55 -0400 Subject: [PATCH 3/5] Add ENVIRONMENT input to rover-group-sync script Now syncs the groups to the appropriate directory based on the ENVIRONMENT environment variable. KFLUXINFRA-3597 --- maintenance/rover-group-sync/README.md | 6 +- .../rover-group-sync/sync-rover-groups.sh | 12 ++- .../test/stubs/find-and-delete-fail | 3 +- .../test/sync-rover-groups.bats | 87 ++++++++++++++++--- 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/maintenance/rover-group-sync/README.md b/maintenance/rover-group-sync/README.md index 0618406..eeed641 100644 --- a/maintenance/rover-group-sync/README.md +++ b/maintenance/rover-group-sync/README.md @@ -1,4 +1,4 @@ -# rover-group-sync +# Rover Group Sync ## `sync-rover-groups.sh` @@ -7,8 +7,8 @@ Bash script intended to run in a Kubernetes CronJob (see `Dockerfile`) It: 1. **Validates** that `oc`, `yq` (mikefarah v4), and `git` are available, and that required paths and environment variables are present. 2. **Prepares LDAP sync config** by copying the LDAP sync template and injecting `LDAP_PASSWORD`, `LDAP_DN`, and the CA path with `yq` in-place edits. 3. **Clones** the target Git repository (branch from `GIT_BRANCH`, default `main`) into a work directory. -4. **Syncs OpenShift Groups from LDAP** with `oc adm groups sync`, normalizes the `List` output with `yq`, and writes **one YAML file per group** under `groups/`, using a filename derived from `metadata.name` (non-alphanumeric characters sanitized with `sed`). -5. **Commits and pushes** only if `groups/` changed; otherwise exits successfully without a commit. +4. **Syncs OpenShift Groups from LDAP** with `oc adm groups sync`, normalizes the `List` output with `yq`, and writes **one YAML file per group** under `groups//`, using a filename derived from `metadata.name` (non-alphanumeric characters sanitized with `sed`). +5. **Commits and pushes** only if `groups/` changed; otherwise exits successfully without a commit. Typical inputs are mounted files (`SYNC_CONFIG_SOURCE`, `LDAP_CA_PATH`, `GIT_REPO_SSH_PATH`) and secrets (`GIT_REPO_URL`, `LDAP_DN`, `LDAP_PASSWORD`, `GIT_PUBLIC_SSH_KEY`). For Git over SSH, the script sets **`StrictHostKeyChecking=yes`** and writes a **temporary `known_hosts`** file whose line is `github.com` plus the key type and base64 key read from **`GIT_SSH_PUBLIC_KEY`**. Do not disable host key verification in production. diff --git a/maintenance/rover-group-sync/sync-rover-groups.sh b/maintenance/rover-group-sync/sync-rover-groups.sh index de7d714..6065ade 100644 --- a/maintenance/rover-group-sync/sync-rover-groups.sh +++ b/maintenance/rover-group-sync/sync-rover-groups.sh @@ -17,8 +17,9 @@ DATE_CMD="${DATE_CMD:-date}" SYNC_CONFIG_SOURCE="${SYNC_CONFIG_SOURCE:-/config/ldap-sync-config.yaml}" LDAP_CA_PATH="${LDAP_CA_PATH:-/secrets/ca.crt}" -GIT_PRIVATE_SSH_PATH="${GIT_PRIVATE_SSH_PATH:-/secrets/git-ssh/ssh_private}" +GIT_PRIVATE_SSH_PATH="${GIT_PRIVATE_SSH_PATH:-/secrets/git-repo/ssh_private}" BRANCH="${GIT_BRANCH:-main}" +ENVIRONMENT="${ENVIRONMENT:-staging}" # Optional: export KUBECONFIG only if your oc build requires an apiserver even for LDAP-only sync. # [[ -n "${KUBECONFIG:-}" ]] || export KUBECONFIG=/var/run/kubeconfig/kubeconfig @@ -49,6 +50,11 @@ for _required in GIT_REPO_URL LDAP_DN LDAP_PASSWORD GIT_SSH_PUBLIC_KEY; do fi done +if [[ "${ENVIRONMENT}" != "staging" && "${ENVIRONMENT}" != "production" ]]; then + echo "ENVIRONMENT must be either staging or production" >&2 + exit 1 +fi + # Inject credentials into LDAP config writable copy; ConfigMap mount is read-only SYNC_CONFIG_FILE="$(mktemp)" trap 'rm -f "${SYNC_CONFIG_FILE}"' EXIT @@ -88,8 +94,8 @@ echo "Retrieving groups from LDAP..." COUNT="$("${YQ}" '.items | length' "${LIST_TMP}")" # Create Group manifests in target directory (and sanitize the group name) -echo "Creating Group manifests in target directory..." -TARGET_DIR="${WORKDIR}/groups" +echo "Creating Group manifests in target ${ENVIRONMENT} directory..." +TARGET_DIR="${WORKDIR}/groups/${ENVIRONMENT}" mkdir -p "${TARGET_DIR}" "${FIND}" "${TARGET_DIR}" -maxdepth 1 -type f -name '*.yaml' -delete diff --git a/maintenance/rover-group-sync/test/stubs/find-and-delete-fail b/maintenance/rover-group-sync/test/stubs/find-and-delete-fail index 9c6d7db..abf118e 100755 --- a/maintenance/rover-group-sync/test/stubs/find-and-delete-fail +++ b/maintenance/rover-group-sync/test/stubs/find-and-delete-fail @@ -4,7 +4,8 @@ REAL_FIND="$(command -v find)" [[ -n "${REAL_FIND}" ]] || { echo "stub find: real find not on PATH" >&2; exit 99; } target="${1:-}" -if [[ "$(basename -- "${target}")" == "groups" ]] && [[ "$*" == *"-name"* ]] && [[ "$*" == *"-delete"* ]]; then +# Script runs find on groups// (e.g. groups/staging), not groups/ itself. +if [[ "$(basename -- "$(dirname -- "${target}")")" == "groups" ]] && [[ "$*" == *"-name"* ]] && [[ "$*" == *"-delete"* ]]; then echo "find: simulated failure deleting existing group yaml files" >&2 exit 1 fi diff --git a/maintenance/rover-group-sync/test/sync-rover-groups.bats b/maintenance/rover-group-sync/test/sync-rover-groups.bats index 8c1c473..f64c7ac 100644 --- a/maintenance/rover-group-sync/test/sync-rover-groups.bats +++ b/maintenance/rover-group-sync/test/sync-rover-groups.bats @@ -20,6 +20,7 @@ setup() { export LDAP_PASSWORD="test-password" export WORKDIR="${test_root}/workdir" export GIT_BRANCH="${GIT_BRANCH:-main}" + export ENVIRONMENT="${ENVIRONMENT:-staging}" SCRIPT="${BATS_TEST_DIRNAME}/../sync-rover-groups.sh" unset REASON unset CASE @@ -124,6 +125,15 @@ stub_binaries() { [[ "${output}" == *"GIT_SSH_PUBLIC_KEY must be set"* ]] } +@test "fails when ENVIRONMENT is neither 'production' nor 'staging'" { + stub_binaries + export GIT_REPO_URL="https://example.invalid/repo.git" + export ENVIRONMENT="not-an-environment" + run bash "${SCRIPT}" + [[ "${status}" -eq 1 ]] + [[ "${output}" == *"ENVIRONMENT must be either staging or production"* ]] +} + # --- missing files --- @test "fails when SYNC_CONFIG_SOURCE file does not exist" { @@ -385,8 +395,8 @@ stub_binaries() { run bash "${SCRIPT}" [[ "${status}" -eq 0 ]] - [[ -f "${WORKDIR}/groups/test-group.yaml" ]] - run yq '.metadata.name' "${WORKDIR}/groups/test-group.yaml" + [[ -f "${WORKDIR}/groups/staging/test-group.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/staging/test-group.yaml" [[ "${output}" == "test-group" ]] run git -C "${bare}" log --oneline -1 @@ -409,20 +419,77 @@ stub_binaries() { run bash "${SCRIPT}" [[ "${status}" -eq 0 ]] - [[ -f "${WORKDIR}/groups/rover-alpha.yaml" ]] - [[ -f "${WORKDIR}/groups/rover-bravo.yaml" ]] + [[ -f "${WORKDIR}/groups/staging/rover-alpha.yaml" ]] + [[ -f "${WORKDIR}/groups/staging/rover-bravo.yaml" ]] - run yq '.metadata.name' "${WORKDIR}/groups/rover-alpha.yaml" + run yq '.metadata.name' "${WORKDIR}/groups/staging/rover-alpha.yaml" [[ "${output}" == "rover-alpha" ]] - run yq '.users | length' "${WORKDIR}/groups/rover-alpha.yaml" + run yq '.users | length' "${WORKDIR}/groups/staging/rover-alpha.yaml" [[ "${output}" == "0" ]] - run yq '.metadata.name' "${WORKDIR}/groups/rover-bravo.yaml" + run yq '.metadata.name' "${WORKDIR}/groups/staging/rover-bravo.yaml" [[ "${output}" == "rover-bravo" ]] - run yq '.users[0]' "${WORKDIR}/groups/rover-bravo.yaml" + run yq '.users[0]' "${WORKDIR}/groups/staging/rover-bravo.yaml" [[ "${output}" == "user-one" ]] } +@test "syncs groups, writes manifests, commits and pushes when GIT_BRANCH is set" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + export GIT_BRANCH="my-branch" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + local tmp_branch + tmp_branch="$(mktemp -d)" + git clone -q "file://${bare}" "${tmp_branch}" + git -C "${tmp_branch}" checkout -b my-branch + git -C "${tmp_branch}" push -q origin my-branch + + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + [[ -f "${WORKDIR}/groups/staging/test-group.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/staging/test-group.yaml" + [[ "${output}" == "test-group" ]] + + run git -C "${bare}" log --oneline -1 my-branch + [[ "${output}" == *"chore(groups): sync rover LDAP groups my-branch"* ]] +} + +@test "syncs groups, writes manifests, commits and pushes when ENVIRONMENT is set" { + [[ -n "$(command -v yq)" ]] || skip "yq not installed" + [[ -n "$(command -v git)" ]] || skip "git not installed" + + export CASE=single + export OC="${BATS_TEST_DIRNAME}/stubs/stub-oc" + chmod +x "${OC}" + export GIT_SSH_COMMAND="true" + export ENVIRONMENT="production" + + bare="$(mktemp -d)/remote.git" + init_bare_repo_with_empty_commit "${bare}" + + export GIT_REPO_URL="file://${bare}" + + run bash "${SCRIPT}" + [[ "${status}" -eq 0 ]] + + [[ -f "${WORKDIR}/groups/production/test-group.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/production/test-group.yaml" + [[ "${output}" == "test-group" ]] + + run git -C "${bare}" log --oneline -1 + [[ "${output}" == *"chore(groups): sync rover LDAP groups"* ]] +} + @test "syncs groups, sanitizes metadata.name into a safe filename (sed), writes manifests, commits and pushes with one group" { [[ -n "$(command -v yq)" ]] || skip "yq not installed" [[ -n "$(command -v git)" ]] || skip "git not installed" @@ -440,8 +507,8 @@ stub_binaries() { [[ "${status}" -eq 0 ]] # konflux:weird/name -> colon and slash become underscores (see sync-rover-groups.sh sed) - [[ -f "${WORKDIR}/groups/konflux_weird_name.yaml" ]] - run yq '.metadata.name' "${WORKDIR}/groups/konflux_weird_name.yaml" + [[ -f "${WORKDIR}/groups/staging/konflux_weird_name.yaml" ]] + run yq '.metadata.name' "${WORKDIR}/groups/staging/konflux_weird_name.yaml" [[ "${output}" == "konflux:weird/name" ]] } From 7aad0aebb791c0d8778c92f76d435942a6318aaf Mon Sep 17 00:00:00 2001 From: Emily Keefe Date: Fri, 24 Apr 2026 14:49:42 -0400 Subject: [PATCH 4/5] Install git in rover-group-sync Dockerfile KFLUXINFRA-3597 --- maintenance/rover-group-sync/Dockerfile | 4 +++- .../rover-group-sync/sync-rover-groups.sh | 16 +++++++++++++--- .../rover-group-sync/test/sync-rover-groups.bats | 6 +++--- .../rover-group-sync/test/test_helpers.bash | 6 ++++-- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/maintenance/rover-group-sync/Dockerfile b/maintenance/rover-group-sync/Dockerfile index 7a7f906..591dc24 100644 --- a/maintenance/rover-group-sync/Dockerfile +++ b/maintenance/rover-group-sync/Dockerfile @@ -10,11 +10,13 @@ RUN curl -fsSL -o /tmp/yq "https://github.com/mikefarah/yq/releases/download/${Y && mv /tmp/yq /usr/local/bin/yq \ && chmod +x /usr/local/bin/yq +# Install git +RUN yum install -y git && yum clean all + # Copy the sync script WORKDIR /scripts COPY sync-rover-groups.sh /scripts/sync-rover-groups.sh - RUN chmod +x /scripts/sync-rover-groups.sh # Run the sync script diff --git a/maintenance/rover-group-sync/sync-rover-groups.sh b/maintenance/rover-group-sync/sync-rover-groups.sh index 6065ade..804b478 100644 --- a/maintenance/rover-group-sync/sync-rover-groups.sh +++ b/maintenance/rover-group-sync/sync-rover-groups.sh @@ -23,9 +23,19 @@ ENVIRONMENT="${ENVIRONMENT:-staging}" # Optional: export KUBECONFIG only if your oc build requires an apiserver even for LDAP-only sync. # [[ -n "${KUBECONFIG:-}" ]] || export KUBECONFIG=/var/run/kubeconfig/kubeconfig -# Check for requirements -if ! command -v "${OC}" >/dev/null || ! command -v "${YQ}" >/dev/null || ! command -v "${GIT}" >/dev/null; then - echo "missing oc, yq, or git in PATH" >&2 +# Check for package requirements +if ! command -v "${OC}" >/dev/null; then + echo "missing oc in PATH" >&2 + exit 1 +fi + +if ! command -v "${YQ}" >/dev/null; then + echo "missing yq in PATH" >&2 + exit 1 +fi + +if ! command -v "${GIT}" >/dev/null; then + echo "missing git in PATH" >&2 exit 1 fi diff --git a/maintenance/rover-group-sync/test/sync-rover-groups.bats b/maintenance/rover-group-sync/test/sync-rover-groups.bats index f64c7ac..c4e4a3c 100644 --- a/maintenance/rover-group-sync/test/sync-rover-groups.bats +++ b/maintenance/rover-group-sync/test/sync-rover-groups.bats @@ -40,7 +40,7 @@ stub_binaries() { export GIT_REPO_URL="https://example.invalid/repo.git" run bash "${SCRIPT}" [[ "${status}" -eq 1 ]] - [[ "${output}" == *"missing oc, yq, or git"* ]] + [[ "${output}" == *"missing yq"* ]] } @test "fails when git is not installed (GIT points to missing file)" { @@ -49,7 +49,7 @@ stub_binaries() { export GIT_REPO_URL="https://example.invalid/repo.git" run bash "${SCRIPT}" [[ "${status}" -eq 1 ]] - [[ "${output}" == *"missing oc, yq, or git"* ]] + [[ "${output}" == *"missing git"* ]] } @test "fails when oc is not installed (OC points to missing file)" { @@ -58,7 +58,7 @@ stub_binaries() { export GIT_REPO_URL="https://example.invalid/repo.git" run bash "${SCRIPT}" [[ "${status}" -eq 1 ]] - [[ "${output}" == *"missing oc, yq, or git"* ]] + [[ "${output}" == *"missing oc"* ]] } # --- empty environment variables --- diff --git a/maintenance/rover-group-sync/test/test_helpers.bash b/maintenance/rover-group-sync/test/test_helpers.bash index 7ae5647..c3b4d08 100644 --- a/maintenance/rover-group-sync/test/test_helpers.bash +++ b/maintenance/rover-group-sync/test/test_helpers.bash @@ -3,15 +3,17 @@ init_bare_repo_with_empty_commit() { local bare="$1" + local no_hooks + no_hooks="$(mktemp -d)" git init --bare -b main "${bare}" local tmp tmp="$(mktemp -d)" ( cd "${tmp}" || exit 1 git init -b main - git commit --allow-empty -m "init" + git -c "core.hooksPath=${no_hooks}" commit --allow-empty -m "init" git remote add origin "file://${bare}" git push -u origin main ) - rm -rf "${tmp}" + rm -rf "${tmp}" "${no_hooks}" } From 152c93b676b4a99b28a69383bbf4edcb97985b86 Mon Sep 17 00:00:00 2001 From: "red-hat-konflux[bot]" <126015336+red-hat-konflux[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:55:34 +0000 Subject: [PATCH 5/5] chore(deps): update google.golang.org/genproto/googleapis/api digest to 7cedc36 Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com> --- maintenance/etcd/build-deps/go.mod | 6 +++--- maintenance/etcd/build-deps/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/maintenance/etcd/build-deps/go.mod b/maintenance/etcd/build-deps/go.mod index dbbe28f..bbcce2f 100644 --- a/maintenance/etcd/build-deps/go.mod +++ b/maintenance/etcd/build-deps/go.mod @@ -33,8 +33,8 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/grpc v1.79.3 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/maintenance/etcd/build-deps/go.sum b/maintenance/etcd/build-deps/go.sum index ea93d22..99bfd52 100644 --- a/maintenance/etcd/build-deps/go.sum +++ b/maintenance/etcd/build-deps/go.sum @@ -124,14 +124,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo= +google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=