Repository: adobe/rules_gitops
Branch: main
Commit: 6387a44155b9
Files: 251
Total size: 462.8 KB
Directory structure:
gitextract_s1k_48ok/
├── .bazelignore
├── .bazelrc
├── .bazelversion
├── .bcr/
│ ├── metadata.template.json
│ ├── presubmit.yml
│ └── source.template.json
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── ci.yaml
│ ├── conventional-commits.yaml
│ ├── publish.yaml
│ ├── release.yml
│ └── release_prep.sh
├── .gitignore
├── BUILD.bazel
├── CODE_OF_CONDUCT.md
├── COPYRIGHT
├── LICENSE
├── MODULE.bazel
├── README.md
├── WORKSPACE.bazel
├── adapters/
│ ├── BUILD.bazel
│ ├── external_image.bzl
│ ├── providers.bzl
│ └── rules_img.bzl
├── e2e/
│ ├── BUILD.bazel
│ ├── deployment/
│ │ ├── BUILD.bazel
│ │ ├── helloworld.go
│ │ └── manifests/
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ └── util/
│ ├── BUILD.bazel
│ ├── kubectl_cmd.sh.tpl
│ ├── setup.sh
│ ├── teardown.sh
│ ├── test.sh
│ └── util.bzl
├── examples/
│ ├── README.md
│ ├── helloworld/
│ │ ├── .bazelrc
│ │ ├── .bazelversion
│ │ ├── BUILD.bazel
│ │ ├── MODULE.bazel
│ │ ├── deployment.yaml
│ │ ├── helloworld.go
│ │ ├── helloworld_test.go
│ │ ├── k8s_deploy_test.sh
│ │ └── service.yaml
│ └── legacy_docker/
│ ├── .bazelrc
│ ├── .bazelversion
│ ├── BUILD.bazel
│ ├── MODULE.bazel
│ ├── WORKSPACE
│ ├── adapters/
│ │ ├── BUILD.bazel
│ │ └── docker.bzl
│ ├── container_contents.txt
│ ├── deployment.yaml
│ └── patches/
│ └── docker.patch
├── gitops/
│ ├── BUILD.bazel
│ ├── analysis/
│ │ ├── BUILD.bazel
│ │ ├── analysis.go
│ │ └── analysis.proto
│ ├── bazel/
│ │ ├── BUILD.bazel
│ │ ├── bazeltargets.go
│ │ └── bazeltargets_test.go
│ ├── blaze_query/
│ │ ├── BUILD.bazel
│ │ └── build.proto
│ ├── commitmsg/
│ │ ├── BUILD.bazel
│ │ ├── commitmsg.go
│ │ └── commitmsg_test.go
│ ├── defs.bzl
│ ├── digester/
│ │ ├── BUILD.bazel
│ │ └── digester.go
│ ├── exec/
│ │ ├── BUILD
│ │ ├── BUILD.bazel
│ │ └── exec.go
│ ├── git/
│ │ ├── BUILD.bazel
│ │ ├── bitbucket/
│ │ │ ├── BUILD.bazel
│ │ │ ├── bitbucket.go
│ │ │ ├── bitbucket_test.go
│ │ │ └── testdata/
│ │ │ └── create_pr.json
│ │ ├── git.go
│ │ ├── github/
│ │ │ ├── BUILD.bazel
│ │ │ └── github.go
│ │ ├── gitlab/
│ │ │ ├── BUILD.bazel
│ │ │ ├── gitlab.go
│ │ │ └── gitlab_test.go
│ │ └── server.go
│ ├── prer/
│ │ ├── BUILD.bazel
│ │ └── create_gitops_prs.go
│ └── private/
│ ├── BUILD.bazel
│ ├── gitops.bzl
│ ├── k8s_deploy.bzl
│ ├── k8s_gitops.sh.tpl
│ ├── nameprefix_deployment_labels_config.yaml
│ ├── namesuffix_deployment_labels_config.yaml
│ └── test/
│ ├── BUILD.bazel
│ ├── deployment.yaml
│ ├── deployment1.yaml
│ ├── goldens/
│ │ ├── deploy_test.golden
│ │ └── external_image_test.golden
│ ├── images/
│ │ ├── BUILD.bazel
│ │ └── container_content.txt
│ └── templates.bzl
├── go.mod
├── go.sum
├── kubectl/
│ ├── BUILD.bazel
│ ├── defs.bzl
│ └── private/
│ ├── BUILD.bazel
│ ├── extension.bzl
│ ├── kubeconfig.bzl
│ ├── kubectl_binary.bzl
│ ├── platforms.bzl
│ ├── providers.bzl
│ ├── resolved_toolchain.bzl
│ ├── run-all.sh.tpl
│ ├── toolchain.bzl
│ └── versions/
│ ├── BUILD.bazel
│ ├── update_versions.go
│ └── versions.bzl
├── kustomize/
│ ├── BUILD.bazel
│ ├── defs.bzl
│ └── private/
│ ├── BUILD.bazel
│ ├── extension.bzl
│ ├── kustomization.bzl
│ ├── kustomize_binary.bzl
│ ├── platforms.bzl
│ ├── providers.bzl
│ ├── resolved_toolchain.bzl
│ ├── show.bzl
│ ├── tests/
│ │ ├── BUILD.bazel
│ │ ├── crb.yaml
│ │ ├── deployment.yaml
│ │ ├── goldens/
│ │ │ └── raw.golden
│ │ └── service.yaml
│ ├── toolchain.bzl
│ └── versions/
│ ├── BUILD.bazel
│ ├── update_versions.go
│ └── versions.bzl
├── renovate.json
├── resolver/
│ ├── BUILD.bazel
│ ├── pkg/
│ │ ├── BUILD.bazel
│ │ ├── resolver.go
│ │ ├── resolver_test.go
│ │ └── testdata/
│ │ ├── cwf.expected.yaml
│ │ ├── cwf.yaml
│ │ ├── digest.expected.yaml
│ │ ├── digest.yaml
│ │ ├── emptyinit.expected.yaml
│ │ ├── emptyinit.yaml
│ │ ├── flinkapp.expected.yaml
│ │ ├── flinkapp.yaml
│ │ ├── happypath.expected.yaml
│ │ ├── happypath.yaml
│ │ ├── zk.expected.yaml
│ │ └── zk.yaml
│ └── resolver.go
├── stamper/
│ ├── BUILD.bazel
│ ├── main.go
│ └── stamp.bzl
├── templating/
│ ├── BUILD.bazel
│ ├── fasttemplate/
│ │ ├── BUILD.bazel
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── example_test.go
│ │ ├── template.go
│ │ └── template_test.go
│ ├── main.go
│ └── testdata/
│ ├── generated
│ ├── stable-status.txt
│ ├── template1.tpl
│ └── volatile-status.txt
├── testing/
│ ├── BUILD.bazel
│ ├── defs.bzl
│ ├── it_manifest_filter/
│ │ ├── BUILD.bazel
│ │ ├── it_manifest_filter.go
│ │ └── pkg/
│ │ ├── BUILD.bazel
│ │ ├── filter.go
│ │ ├── filter_test.go
│ │ └── testdata/
│ │ ├── certificate.expected.yaml
│ │ ├── certificate.yaml
│ │ ├── happypath.expected.yaml
│ │ ├── happypath.yaml
│ │ ├── statefulset.expected.yaml
│ │ ├── statefulset.yaml
│ │ ├── statefulset2.expected.yaml
│ │ ├── statefulset2.yaml
│ │ ├── statefulset3.expected.yaml
│ │ └── statefulset3.yaml
│ ├── it_sidecar/
│ │ ├── BUILD.bazel
│ │ ├── client/
│ │ │ ├── BUILD.bazel
│ │ │ ├── noop.setup
│ │ │ ├── sidecar_client.go
│ │ │ ├── test_callback/
│ │ │ │ ├── BUILD.bazel
│ │ │ │ └── sidecar_client_test.go
│ │ │ └── test_no_callback/
│ │ │ ├── BUILD.bazel
│ │ │ └── sidecar_client_test_no_callback.go
│ │ ├── it_sidecar.go
│ │ └── stern/
│ │ ├── BUILD.bazel
│ │ ├── container_state.go
│ │ ├── main.go
│ │ ├── tail.go
│ │ └── watch.go
│ └── private/
│ ├── BUILD.bazel
│ ├── k8s_test_namespace.bzl
│ ├── k8s_test_namespace.sh.tpl
│ ├── k8s_test_setup.bzl
│ └── set_namespace.sh
├── tests/
│ ├── BUILD.bazel
│ ├── configmaps/
│ │ ├── mapa/
│ │ │ ├── mapa1.properties
│ │ │ └── mapa2.properties
│ │ └── mapb/
│ │ └── config.yaml
│ ├── crb.yaml
│ ├── deployment.yaml
│ ├── deployment_with_labels.yaml
│ ├── expected_image_resolved_test.tpl.yaml
│ ├── expected_patch.tpl.yaml
│ ├── goldens/
│ │ ├── common_labels.golden.yaml
│ │ ├── configmap.golden.yaml
│ │ ├── deployment_prefix.golden.yaml
│ │ ├── deployment_prefix_compat.golden.yaml
│ │ ├── deployment_suffix.golden.yaml
│ │ ├── deployment_suffix_compat.golden.yaml
│ │ ├── image_digest.golden.yaml
│ │ ├── image_resolved.golden.yaml
│ │ ├── name_prefix.golden.yaml
│ │ ├── name_suffix.golden.yaml
│ │ ├── namespace.golden.yaml
│ │ ├── patch.golden.yaml
│ │ ├── patch_images.golden.yaml
│ │ ├── raw.golden.yaml
│ │ └── secrets.golden.yaml
│ ├── images/
│ │ ├── BUILD.bazel
│ │ └── container_content.txt
│ ├── job.yaml
│ ├── overlay/
│ │ └── deployment.yaml
│ ├── secrets/
│ │ ├── secreta/
│ │ │ └── file1.yaml
│ │ └── secretb/
│ │ ├── file2.txt
│ │ └── foo
│ ├── service.yaml
│ ├── set_namespace_test.sh
│ ├── test.yaml
│ └── test_expected.yaml
└── tools/
├── BUILD.bazel
├── bazel
├── preset.bzl
├── preset7.bazelrc
├── preset8.bazelrc
├── preset9.bazelrc
└── util.bzl
================================================
FILE CONTENTS
================================================
================================================
FILE: .bazelignore
================================================
examples/
.git/
================================================
FILE: .bazelrc
================================================
########################
# Import bazelrc presets
import %workspace%/tools/preset.bazelrc
# Don’t want to push a rules author to update their deps if not needed.
# https://bazel.build/reference/command-line-reference#flag--check_direct_dependencies
# https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0
common --config=ruleset
# Bazel settings that apply to this repository.
# Take care to document any settings that you expect users to apply.
# Settings that apply only to CI are in .github/workflows/ci.bazelrc
# Required until this is the default; expected in Bazel 7
common --enable_bzlmod
# C++17 is required by protobuf and abseil-cpp
build --cxxopt=-std=c++17
build --host_cxxopt=-std=c++17
# Ensure that the MODULE.bazel.lock file is complete and committed.
# This is an important security measure: it ensures that developers on the
# same rule set download dependencies at the same versions with the same bits.
# This setting does not affect modules that depend on this module.
#
# When updating dependencies, use --lockfile_mode=refresh, for example:
# bazel mod tidy --lockfile_mode=refresh
#
# When testing different versions of Bazel, use --lockfile_mode=update or
# --lockfile_mode=off. The lock file format changes over time, and different
# versions of Bazel may expect different syntax. Bazel also implicitly requires
# some modules, and different versions have different dependencies, which
# also affects the contents of the lock file.
common --lockfile_mode=off
# This directory is configured in GitHub actions to be persisted between runs.
# We do not enable the repository cache to cache downloaded external artifacts
# as these are generally faster to download again than to fetch them from the
# GitHub actions cache.
common:ci --disk_cache=~/.cache/bazel
common --@protobuf//bazel/toolchains:prefer_prebuilt_protoc
# Load any settings specific to the current user.
# .bazelrc.user should appear in .gitignore so that settings are not shared with team members
# This needs to be last statement in this
# config, as the user configuration should be able to overwrite flags from this file.
# See https://docs.bazel.build/versions/master/best-practices.html#bazelrc
# (Note that we use .bazelrc.user so the file appears next to .bazelrc in directory listing,
# rather than user.bazelrc as suggested in the Bazel docs)
try-import %workspace%/.bazelrc.user
================================================
FILE: .bazelversion
================================================
7.6.0
================================================
FILE: .bcr/metadata.template.json
================================================
{
"homepage": "https://github.com/adobe/rules_gitops",
"maintainers": [
{
"name": "Nick Schaap",
"email": "schaap@adobe.com",
"github": "nickschaap",
"github_user_id": 4684894
},
{
"name": "Konstantin Zadorozhny",
"email": "zadorozh@adobe.com",
"github": "kzadorozhny",
"github_user_id": 1616702
}
],
"repository": ["github:adobe/rules_gitops"],
"versions": [],
"yanked_versions": {}
}
================================================
FILE: .bcr/presubmit.yml
================================================
bcr_test_module:
module_path: '.'
matrix:
bazel: ['7.x', '8.x', '9.x']
platform: ['debian10', 'macos', 'ubuntu2004']
tasks:
run_tests:
name: 'Run test module'
bazel: ${{ bazel }}
platform: ${{ platform }}
test_flags:
- "--test_tag_filters=-skip-bazel-ci"
test_targets:
- '//...'
================================================
FILE: .bcr/source.template.json
================================================
{
"integrity": "**leave this alone**",
"strip_prefix": "{REPO}-{VERSION}",
"docs_url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/{REPO}-{TAG}.docs.tar.gz",
"url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/{REPO}-{TAG}.tar.gz"
}
================================================
FILE: .gitattributes
================================================
#################################
# Configuration for 'git archive'
# See https://git-scm.com/docs/git-archive#ATTRIBUTES
# Don't include examples in the distribution artifact, to reduce size.
# You may want to add additional exclusions for folders or files that users don't need.
examples export-ignore
tests export-ignore
================================================
FILE: .github/CODEOWNERS
================================================
#
# The default owners for everything in the repo.
#
* @adobe/rules_gitops
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
Thanks for choosing to contribute!
The following are a set of guidelines to follow when contributing to this project.
## Code Of Conduct
This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating,
you are expected to uphold this code. Please report unacceptable behavior to
[Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com).
## Have A Question?
Start by filing an issue. The existing committers on this project work to reach
consensus around project direction and issue solutions within issue threads
(when appropriate).
## Contributor License Agreement
All third-party contributions to this project must be accompanied by a signed contributor
license agreement. This gives Adobe permission to redistribute your contributions
as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You
only need to submit an Adobe CLA one time, so if you have submitted one previously,
you are good to go!
## Code Reviews
All submissions should come in the form of pull requests and need to be reviewed
by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/)
for more information on sending pull requests.
Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when
submitting a pull request!
## From Contributor To Committer
We love contributions from our community! If you'd like to go a step beyond contributor
and become a committer with full write access and a say in the project, you must
be invited to the project. The existing committers employ an internal nomination
process that must reach lazy consensus (silence is approval) before invitations
are issued. If you feel you are qualified and want to get more deeply involved,
feel free to reach out to existing committers to have a conversation about that.
## Security Issues
Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html).
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
### Description of the problem / feature request:
> Replace this line with your answer.
### Feature requests: what underlying problem are you trying to solve with this feature?
> Replace this line with your answer.
### Bugs: what's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.
> Replace this line with your answer.
### What operating system are you running Bazel on?
> Replace this line with your answer.
### What's the output of `bazel info release`?
> Replace this line with your answer.
### If `bazel info release` returns "development version" or "(@non-git)", tell us how you built Bazel.
> Replace this line with your answer.
### Any other information, logs, or outputs that you want to share?
> Replace these lines with your answer.
>
> If the files are large, upload as attachment or provide link.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
## Related Issue
## Motivation and Context
## How Has This Been Tested?
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist:
- [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html).
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI
on:
push:
branches:
- main
- feature/*
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-24.04
strategy:
matrix:
bazel-version:
- 7.6.0
- 8.5.0
- 9.0.0
env:
USE_BAZEL_VERSION: ${{ matrix.bazel-version }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Mount Bazel cache
uses: actions/cache@v4
with:
path: ~/.cache/bazel
key: bazel-${{ matrix.bazel-version }}-${{ github.sha }}
restore-keys: |
bazel-${{ matrix.bazel-version }}-
bazel-
- name: Setup build tools
run: |
TOOLS_DIR="${HOME}/.cache/tools"
mkdir -p "${TOOLS_DIR}/bin/"
curl -Ls -o "${TOOLS_DIR}/bin/bazel" "https://github.com/bazelbuild/bazelisk/releases/download/v1.14.0/bazelisk-linux-amd64"
chmod +x "${TOOLS_DIR}/bin/bazel"
echo "${TOOLS_DIR}/bin" >> $GITHUB_PATH
- name: Test
run: |
bazel --output_base=~/.bazel/ test //... --config=ci
- name: Build helloworld
run: |
cd examples/helloworld
bazel --output_base=~/.bazel/ build //...
- name: Build legacy docker
if: matrix.bazel-version == '7.6.0'
run: |
cd examples/legacy_docker
bazel --output_base=~/.bazel/ build //...
================================================
FILE: .github/workflows/conventional-commits.yaml
================================================
# This helps the tag.yaml action to automatically create new releases
#
# tag.yaml requires all commits to follow the conventional commit pattern so that it can
# automatically derive the next release version based on the commit history
name: Verify PR title/description
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/publish.yaml
================================================
# Publish new releases to Bazel Central Registry.
name: Publish to BCR
on:
# Run the publish workflow after a successful release
# Will be triggered from the release.yaml workflow
workflow_call:
inputs:
tag_name:
required: true
type: string
secrets:
publish_token:
required: true
# In case of problems, let release engineers retry by manually dispatching
# the workflow from the GitHub UI
workflow_dispatch:
inputs:
tag_name:
description: git tag being released
required: true
type: string
jobs:
publish:
uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.0.0
with:
tag_name: ${{ inputs.tag_name }}
# GitHub repository which is a fork of the upstream where the Pull Request will be opened.
registry_fork: adobe/bazel-central-registry
permissions:
attestations: write
contents: write
id-token: write
secrets:
# Necessary to push to the BCR fork, and to open a pull request against a registry
publish_token: ${{ secrets.publish_token || secrets.BCR_PUBLISH_TOKEN }}
================================================
FILE: .github/workflows/release.yml
================================================
# Cut a release whenever a new tag is pushed to the repo.
name: Release
on:
# Can be triggered from the tag.yaml workflow
workflow_call:
inputs:
tag_name:
required: true
type: string
secrets:
publish_token:
required: true
# Or, developers can manually push a tag from their clone
push:
tags:
- "v*.*.*"
permissions:
id-token: write
attestations: write
contents: write
jobs:
release:
uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7.2.2
with:
release_files: rules_gitops-*.tar.gz
prerelease: false
tag_name: ${{ inputs.tag_name || github.ref_name }}
publish:
needs: release
uses: ./.github/workflows/publish.yaml
with:
tag_name: ${{ inputs.tag_name || github.ref_name }}
secrets:
publish_token: ${{ secrets.publish_token || secrets.BCR_PUBLISH_TOKEN }}
================================================
FILE: .github/workflows/release_prep.sh
================================================
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
# Argument provided by reusable workflow caller, see
# https://github.com/bazel-contrib/.github/blob/d197a6427c5435ac22e56e33340dff912bc9334e/.github/workflows/release_ruleset.yaml#L72
TAG=$1
# The prefix is chosen to match what GitHub generates for source archives
# This guarantees that users can easily switch from a released artifact to a source archive
# with minimal differences in their code (e.g. strip_prefix remains the same)
PREFIX="rules_gitops-${TAG:1}"
ARCHIVE="rules_gitops-$TAG.tar.gz"
# NB: configuration for 'git archive' is in /.gitattributes
git archive --format=tar --prefix=${PREFIX}/ ${TAG} | gzip > $ARCHIVE
SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}')
# Add generated API docs to the release, see https://github.com/bazelbuild/bazel-central-registry/issues/5593
docs="$(mktemp -d)"; targets="$(mktemp)"
bazel --output_base="$docs" query --output=label --output_file="$targets" 'kind("starlark_doc_extract rule", //...)'
bazel --output_base="$docs" build --target_pattern_file="$targets"
tar --create --auto-compress \
--directory "$(bazel --output_base="$docs" info bazel-bin)" \
--file "$GITHUB_WORKSPACE/${ARCHIVE%.tar.gz}.docs.tar.gz" .
cat << EOF
## Using Bzlmod with Bazel 7 or greater
Add to your \`MODULE.bazel\` file:
\`\`\`starlark
bazel_dep(name = "rules_gitops", version = "${TAG:1}")
\`\`\`
EOF
================================================
FILE: .gitignore
================================================
.vscode/
.idea/
.ijwb/
.DS_Store
Thumbs.db
bazel-*
.bazelrc.user
================================================
FILE: BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
# gazelle:exclude examples
# gazelle:map_kind bzl_library bzl_library @bazel_lib//:bzl_library.bzl
load("@buildifier_prebuilt//:rules.bzl", "buildifier", "buildifier_test")
load("@gazelle//:def.bzl", "DEFAULT_LANGUAGES", "gazelle", "gazelle_binary")
load("@package_metadata//licenses/rules:license.bzl", "license")
load("@package_metadata//purl:purl.bzl", "purl")
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")
load("//kustomize:defs.bzl", "kustomize_binary")
buildifier(
name = "buildifier.fix",
exclude_patterns = ["./.git/*"],
lint_mode = "fix",
mode = "fix",
)
buildifier_test(
name = "buildifier_test",
exclude_patterns = ["./.git/*"],
lint_mode = "warn",
mode = "check",
no_sandbox = True,
workspace = ":MODULE.bazel",
)
package_metadata(
name = "package_metadata",
purl = purl.bazel(
module_name(),
module_version(),
),
visibility = ["//visibility:public"],
)
license(
name = "license",
kind = "@package_metadata//licenses/spdx:Apache-2.0",
text = "LICENSE",
)
gazelle_binary(
name = "gazelle_bin",
languages = DEFAULT_LANGUAGES + [
"@bazel_skylib_gazelle_plugin//bzl",
],
)
gazelle(
name = "gazelle",
gazelle = "gazelle_bin",
)
kustomize_binary(
name = "kustomize_bin",
)
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Adobe Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language.
* Being respectful of differing viewpoints and experiences.
* Gracefully accepting constructive criticism.
* Focusing on what is best for the community.
* Showing empathy towards other community members.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances.
* Trolling, insulting/derogatory comments, and personal or political attacks.
* Public or private harassment.
* Publishing others' private information, such as a physical or electronic
address, without explicit permission.
* Other conduct which could reasonably be considered inappropriate in a
professional setting.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at Grp-opensourceoffice@adobe.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [https://contributor-covenant.org/version/1/4][version].
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/
================================================
FILE: COPYRIGHT
================================================
© Copyright 2015-2026 Adobe. All rights reserved.
Adobe holds the copyright for all the files found in this repository.
See the LICENSE file for licensing information.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Adobe
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------------------------------------------------------------------
The contents of third party dependencies in /vendor folder are covered
by their repositories' respective licenses.
The contents of /templating/fasttemplate folder are licensed under
MIT License.
================================================
FILE: MODULE.bazel
================================================
"adobe/rules_gitops"
module(
name = "rules_gitops",
version = "",
compatibility_level = 0,
)
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "package_metadata", version = "0.0.6")
bazel_dep(name = "protobuf", version = "33.4")
bazel_dep(name = "rules_go", version = "0.59.0")
bazel_dep(name = "rules_shell", version = "0.6.1")
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.24.12")
bazel_dep(name = "gazelle", version = "0.47.0")
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(go_deps, "com_github_ghodss_yaml", "com_github_google_go_cmp", "com_github_google_go_github_v32", "com_github_xanzy_go_gitlab", "io_k8s_api", "io_k8s_apimachinery", "io_k8s_client_go", "io_k8s_sigs_kind", "org_golang_google_protobuf", "org_golang_x_oauth2")
bazel_dep(name = "bazel_skylib", version = "1.8.2")
bazel_dep(name = "bazel_lib", version = "3.0.0")
bazel_dep(name = "bazelrc-preset.bzl", version = "1.6.0")
kustomize = use_extension("//kustomize:defs.bzl", "kustomize")
use_repo(kustomize, "kustomize")
register_toolchains("@kustomize//toolchains:all")
kubectl = use_extension("//kubectl:defs.bzl", "kubectl")
use_repo(kubectl, "kubectl")
register_toolchains("@kubectl//toolchains:all")
bazel_dep(name = "rules_img", version = "0.3.3")
# Dev Dependencies
bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.8.2", dev_dependency = True)
bazel_dep(name = "buildifier_prebuilt", version = "8.2.1", dev_dependency = True)
pull = use_repo_rule("@rules_img//img:pull.bzl", "pull")
pull(
name = "alpine",
dev_dependency = True,
digest = "sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375",
layer_handling = "lazy",
registries = [
"mirror.gcr.io",
"index.docker.io",
],
repository = "library/alpine",
tag = "3.23",
)
preset = use_repo_rule("//tools:preset.bzl", "preset")
preset(
name = "preset.bzl",
)
================================================
FILE: README.md
================================================
# Bazel GitOps Rules

Bazel GitOps Rules provides tooling to bridge the gap between Bazel (for hermetic, reproducible, container builds) and continuous, git-operation driven, deployments. Users author standard Kubernetes manifests and kustomize overlays for their services. Bazel GitOps Rules handles image push and substitution, applies necessary kustomizations, and handles content addressed substitutions of all object references (configmaps, secrets, etc). Bazel targets are exposed for applying the rendered manifest directly to a Kubernetes cluster, or into version control facilitating deployment via Git operations.
## Features
- **Kustomize Integration**: Full [Kustomize](https://kustomize.io/) capabilities for generating and transforming manifests
- **GitOps Workflow**: Native support for Git-based deployment workflows with automatic PR creation
- **Container Image Management**: Seamless integration with [rules_img](https://github.com/bazel-contrib/rules_img) for building and pushing container images
- **Namespace Deployments**: Support for personal and team namespace deployments
- **Integration Testing**: Built-in utilities for Kubernetes integration test setup
- **Toolchain Support**: Managed kustomize and kubectl toolchains via Bzlmod
## Ruleset Overview
The ruleset is organized into the following modules:
| Module | Description |
|--------|-------------|
| `@rules_gitops//gitops:defs.bzl` | Core deployment rules (`k8s_deploy`, `gitops`) for rendering manifests and managing deployments |
| `@rules_gitops//kustomize:defs.bzl` | Kustomize rules and toolchain for manifest transformation |
| `@rules_gitops//kubectl:defs.bzl` | Kubectl toolchain for cluster interactions |
| `@rules_gitops//testing:defs.bzl` | Integration testing utilities (`k8s_test_setup`, `k8s_test_namespace`) |
| `@rules_gitops//adapters:providers.bzl` | `K8sPushInfo` provider for container image integration |
| `@rules_gitops//adapters:rules_img.bzl` | Adapter for [rules_img](https://github.com/bazel-contrib/rules_img) container images |
## Getting Started
### Installation
Add `rules_gitops` to your `MODULE.bazel`:
```starlark
bazel_dep(name = "rules_gitops", version = "1.0.0")
```
### Toolchain Setup
The ruleset provides managed toolchains for kustomize and kubectl. To use custom versions, configure the extensions in your `MODULE.bazel`:
```starlark
kustomize = use_extension("@rules_gitops//kustomize:defs.bzl", "kustomize")
kustomize.toolchain(
version = "5.7.1", # specify your desired version
)
kubectl = use_extension("@rules_gitops//kubectl:defs.bzl", "kubectl")
kubectl.toolchain(
version = "1.32.2", # specify your desired version
)
```
## k8s_deploy
The `k8s_deploy` macro creates rules that produce the `.apply` and `.gitops` targets. `k8s_deploy` takes the files listed in the `manifests`, `patches`, and `configmaps_srcs` attributes and combines (**renders**) them into one YAML file. This happens when you `bazel build` or `bazel run` a target created by the `k8s_deploy`. The file is created at `bazel-bin/path/to/package/name.yaml`. When you run a `.apply` target, it runs `kubectl apply` on this file. When you run a `.gitops` target, it copies this file to the appropriate location in the same or separate repository.
For example, let's look at the [example's k8s_deploy](./examples/helloworld/BUILD.bazel). We can peek at the file containing the rendered K8s manifests:
```bash
cd examples
bazel run //:mynamespace.show
```
### Container Image Integration
`rules_gitops` works with [rules_img](https://github.com/bazel-contrib/rules_img) for container image building and pushing. Use the `k8s_push_info` adapter to connect your images to deployments:
```starlark
load("@rules_gitops//adapters:rules_img.bzl", "k8s_push_info")
load("@rules_gitops//gitops:defs.bzl", "k8s_deploy")
load("@rules_img//img:image.bzl", "image_manifest")
load("@rules_img//img:push.bzl", "image_push")
image_manifest(
name = "my_image",
# ... image configuration
)
image_push(
name = "push",
image = ":my_image",
registry = "my-registry.com",
repository = "my-org/my-image",
)
k8s_push_info(
name = "k8s_image",
image = ":my_image",
push = ":push",
registry = "my-registry.com",
repository = "my-org/my-image",
)
k8s_deploy(
name = "my_deployment",
cluster = "my-cluster",
images = [":k8s_image"],
manifests = ["deployment.yaml"],
namespace = "{BUILD_USER}",
)
```
### Basic Usage
Once configured, you can use the generated targets:
```bash
# Show rendered manifests (useful for debugging)
bazel run //path/to:my_deployment.show
# Apply manifests to cluster using your kubectl toolchain
bazel run //path/to:my_deployment.apply
# Generate GitOps output
bazel run //path/to:my_deployment.gitops
```
See the [examples/helloworld](./examples/helloworld) directory for a complete working example.
## GitOps Workflow
The `create_gitops_prs` tool automates the creation of GitOps pull requests as part of your CI pipeline:
The simplified CI pipeline that incorporates GitOps will look like this:
```
[Checkout Code] -> [Bazel Build & Test] -> (if GitOps source branch) -> [Create GitOps PRs]
```
The *Create GitOps PRs* step usually is the last step of a CI pipeline. `rules_gitops` provides the `create_gitops_prs` command line tool that automates the process of creating pull requests.
For the full list of `create_gitops_prs` command line options, run:
```bash
bazel run @rules_gitops//gitops/prer:create_gitops_prs
```
### Supported Git Servers
The `--git_server` parameter defines the type of a Git server API to use. The supported Git server types are `github`, `gitlab`, and `bitbucket`.
Depending on the Git server type the `create_gitops_prs` tool will use following command line parameters:
--git_server | Parameter | Default
------------ | ------------------------------------ | --------------
| `github`
| | ***--github_repo_owner*** | ``
| | ***--github_repo*** | ``
| | ***--github_access_token*** | `$GITHUB_TOKEN`
| | ***--github_enterprise_host*** | ``
| `gitlab` |
| | ***--gitlab_host*** | `https://gitlab.com`
| | ***--gitlab_repo*** | ``
| | ***--gitlab_access_token*** | `$GITLAB_TOKEN`
| `bitbucket`
| | ***--bitbucket_api_pr_endpoint*** | ``
| | ***--bitbucket_user*** | `$BITBUCKET_USER`
| | ***--bitbucket_password*** | `$BITBUCKET_PASSWORD`
## Trunk Based GitOps Workflow
Let's assume the CI build pipeline described above is running the build for `https://github.com/example/repo.git`. In a trunk based branching model, all feature branches are merged into the `main` branch first. The *Create GitOps PRs* step runs on a `main` branch change. The GitOps deployments source files are located in the same repository under the `/cloud` directory.
The *Create GitOps PRs* pipeline step shell command will look like following:
```bash
GIT_ROOT_DIR=$(git rev-parse --show-toplevel)
GIT_COMMIT_ID=$(git rev-parse HEAD)
GIT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
if [ "${GIT_BRANCH_NAME}" == "master"]; then
bazel run //gitops/prer:create_gitops_prs -- \
--workspace $GIT_ROOT_DIR \
--git_repo https://github.com/example/repo.git \
--git_mirror $GIT_ROOT_DIR/.git \
--git_server github \
--release_branch master \
--gitops_pr_into master \
--gitops_pr_title "This is my pull request title" \
--gitops_pr_body "This is my pull request body message" \
--branch_name ${GIT_BRANCH_NAME} \
--git_commit ${GIT_COMMIT_ID} \
fi
```
The `GIT_*` variables describe the current state of the Git repository.
The `--git_repo` parameter defines the remote repository URL. In this case remote repository matches the repository of the working copy. The `--git_mirror` parameter is an optimization used to speed up the target repository clone process using reference repository (see `git clone --reference`). The `--git-server` parameter selects the type of Git server.
The `--release_branch` specifies the value of the ***release_branch_prefix*** attribute of `gitops` targets (see [k8s_deploy](#k8s_deploy)). The `--gitops_pr_into` defines the target branch for newly created pull requests. The `--branch_name` and `--git_commit` are the values used in the pull request commit message.
The `create_gitops_prs` tool will query all `gitops` targets which have set the ***deploy_branch*** attribute (see [k8s_deploy](#k8s_deploy)) and the ***release_branch_prefix*** attribute value that matches the `release_branch` parameter.
The all discovered `gitops` targets are grouped by the value of ***deploy_branch*** attribute. The one deployment branch will accumulate the output of all corresponding `gitops` targets.
For example, we define two deployments: grafana and prometheus. Both deployments share the same namespace. The deployments are grouped by namespace.
```starlark
[
k8s_deploy(
name = NAME,
deploy_branch = NAMESPACE,
...
)
for NAME, CLUSTER, NAMESPACE in [
...
("stage-grafana", "stage", "monitoring-stage"),
("prod-grafana", "prod", "monitoring-prod"),
]
]
[
k8s_deploy(
name = NAME,
deploy_branch = NAMESPACE,
...
)
for NAME, CLUSTER, NAMESPACE in [
...
("stage-prometheus", "stage", "monitoring-stage"),
("prod-prometheus", "prod", "monitoring-prod"),
]
]
```
As a result of the setup above the `create_gitops_prs` tool will open up to 2 potential deployment pull requests:
* from `deploy/monitoring-stage` to `main` including manifests for `stage-grafana` and `stage-prometheus`
* from `deploy/monitoring-prod` to `main` including manifests for `prod-grafana` and `prod-prometheus`
The GitOps pull request is only created (or new commits added) if the `gitops` target changes the state for the target deployment branch. The source pull request will remain open (and keep accumulation GitOps results) until the pull request is merged and source branch is deleted.
The `--stamp` parameter allows for the replacement of certain placeholders, but only when the `gitops` target changes the output's digest compared to the one already saved. The new digest of the unstamped data is also saved with the manifest. The digest is kept in a file in the same location as the YAML file, with a `.digest` extension added to its name. This is helpful when the manifests have volatile information that shouldn't be the only factor causing changes in the target deployment branch.
Here are the placeholders that can be replaced:
| Placeholder | Replacement |
|------------------|-------------------------------------------------|
| `{{GIT_REVISION}}` | Result of `git rev-parse HEAD` |
| `{{UTC_DATE}}` | Result of `date -u` |
| `{{GIT_BRANCH}}` | The `branch_name` argument given to `create_gitops_prs` |
`--dry_run` parameter can be used to test the tool without creating any pull requests. The tool will print the list of the potential pull requests. It is recommended to run the tool in the dry run mode as a part of the CI test suite to verify that the tool is configured correctly.
## Multiple Release Branches GitOps Workflow
In the situation when the trunk based branching model in not suitable the `create_gitops_prs` tool supports creating GitOps pull requests before the code is merged to the `main` branch.
Both trunk and release branch workflows can coexist in the same repository.
For example, let's assume the CI build pipeline described above is running the build for `https://github.com/example/repo.git`. In a release branch branching model, features are merged into multiple target release branches. The release brach name convention is `release/team-`. The *Create GitOps PRs* step runs on release branch changes. GitOps deployments source files are located in the same repository `/cloud` directory in the `main` branch.
The *Create GitOps PRs* pipeline step shell command will look like following:
```bash
GIT_ROOT_DIR=$(git rev-parse --show-toplevel)
GIT_COMMIT_ID=$(git rev-parse HEAD)
GIT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) # => release/team-20200101
RELEASE_BRANCH_SUFFIX=${GIT_BRANCH_NAME#"release/team"} # => -20200101
RELEASE_BRANCH=${GIT_BRANCH_NAME%${RELEASE_BRANCH_SUFFIX}} # => release/team
if [ "${RELEASE_BRANCH}" == "release/team"]; then
bazel run //gitops/prer:create_gitops_prs -- \
--workspace $GIT_ROOT_DIR \
--git_repo https://github.com/example/repo.git \
--git_mirror $GIT_ROOT_DIR/.git \
--git_server github \
--release_branch ${RELEASE_BRANCH} \
--deployment_branch_suffix=${RELEASE_BRANCH_SUFFIX} \
--gitops_pr_into master \
--gitops_pr_title "This is my pull request title" \
--gitops_pr_body "This is my pull request body message" \
--branch_name ${GIT_BRANCH_NAME} \
--git_commit ${GIT_COMMIT_ID} \
fi
```
The meaning of the parameters is the same as with [trunk based workflow](#trunk_based_gitops_workflow).
The `--release_branch` parameter takes the value of `release/team`. The additional parameter `--deployment_branch_suffix` will add the release branch suffix to the target deployment branch name.
If we modify the previous example:
```starlark
[
k8s_deploy(
name = NAME,
deploy_branch = NAMESPACE,
release_branch_prefix = "release/team", # will only be selected when --release_branch=release/team
...
)
for NAME, CLUSTER, NAMESPACE in [
...
("stage-grafana", "stage", "monitoring-stage"),
("prod-grafana", "prod", "monitoring-prod"),
]
]
[
k8s_deploy(
name = NAME,
deploy_branch = NAMESPACE,
release_branch_prefix = "release/team", # will only be selected when --release_branch=release/team
...
)
for NAME, CLUSTER, NAMESPACE in [
...
("stage-prometheus", "stage", "monitoring-stage"),
("prod-prometheus", "prod", "monitoring-prod"),
]
]
```
The result of the setup above the `create_gitops_prs` tool will open up to 2 potential deployment pull requests per release branch. Assuming release branch name is `release/team-20200101`:
* from `deploy/monitoring-stage-20200101` to `master` including manifests for `stage-grafana` and `stage-prometheus`
* from `deploy/monitoring-prod-20200101` to `master` including manifests for `prod-grafana` and `prod-prometheus`
## Integration Testing Support
**Note:** the Integration testing support has known limitations and should be considered **experimental**. The public API will not abide by semver.
Integration tests are defined in `BUILD.bazel` files like this:
```starlark
k8s_test_setup(
name = "service_it.setup",
kubeconfig = "@k8s_test//:kubeconfig",
objects = [
"//service:mynamespace",
],
)
java_test(
name = "service_it",
srcs = [
"ServiceIT.java",
],
data = [
":service_it.setup",
],
jvm_flags = [
"-Dk8s.setup=$(location :service_it.setup)",
],
# other attributes omitted for brevity
)
```
The test is composed of two rules, a `k8s_test_setup` rule to manage the Kubernetes setup and a `java_test` rule that executes the actual test.
The `k8s_test_setup` rule produces a shell script which creates a temporary namespace (the namespace name is your username followed by five random digits) and creates a kubeconfig file that allows access to this new namespace. Inside the namespace, it creates some objects specified in the `objects` attributes. In the example, there is one target here: `//service:mynamespace`. This target represents a file containing all the Kubernetes object manifests required to run the service.
The output of the `k8s_test_setup` rule (a shell script) is referenced in the `java_test` rule. It's listed under the `data` attribute, which declares the target as a dependency, and is included in the jvm flags in this clause: `$(location :service_it.setup)`. The "location" function is specific to Bazel: given a target, it returns the path to the file produced by that target. In this case, it returns the path to the shell script created by our `k8s_test_setup` rule.
The test code launches the script to perform the test setup. The test code should also monitor the script console output to listen to the pod readiness events.
The `@k8s_test//:kubeconfig` target referenced from `k8s_test_setup` rule serves the purpose of making Kubernetes configuration available in the test sandbox. The `kubeconfig` repository rule in the `WORKSPACE` file will need, at minimum, provide the cluster name.
```starlark
load("//gitops:defs.bzl", "kubeconfig")
kubeconfig(
name = "k8s_test",
cluster = "dev",
)
```
## Building & Testing
### Building & Testing GitOps Rules
```bash
bazel test //...
```
### Building & Testing Examples Project
```bash
cd examples/helloworld
bazel test //...
```
## Have a Question
Find the `rules_gitops` contributors in the [#gitops](https://bazelbuild.slack.com/archives/C01SF68MTFS) channel on the [Bazel Slack](https://slack.bazel.build/).
## Contributing
Contributions are welcomed! Read the [Contributing Guide](./.github/CONTRIBUTING.md) for more information.
## Licensing
The contents of [/templating/fasttemplate](./templating/fasttemplate) are licensed under MIT License. See [LICENSE](./templating/fasttemplate/LICENSE) for more information.
All other files are licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
================================================
FILE: WORKSPACE.bazel
================================================
# Marker that this is the root of a Bazel workspace.
================================================
FILE: adapters/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
bzl_library(
name = "providers",
srcs = ["providers.bzl"],
visibility = ["//visibility:public"],
)
bzl_library(
name = "rules_img",
srcs = ["rules_img.bzl"],
visibility = ["//visibility:public"],
deps = [
":providers",
"@rules_img//img:providers",
],
)
bzl_library(
name = "external_image",
srcs = ["external_image.bzl"],
visibility = ["//visibility:public"],
deps = [":providers"],
)
================================================
FILE: adapters/external_image.bzl
================================================
"""
Implementation of external image information provider suitable for injection into manifests
"""
load(":providers.bzl", "K8sPushInfo")
def _external_image_impl(ctx):
sv = ctx.attr.image.split("@", 1)
if (len(sv) == 1) and (not ctx.attr.digest):
fail("digest must be specified either in image or as a separate attribute")
s = sv[0].split(":", 1)[0] #drop tag
registry, repository = s.split("/", 1)
#write digest to a file
digest_file = ctx.actions.declare_file(ctx.label.name + ".digest")
ctx.actions.write(
output = digest_file,
content = ctx.attr.digest,
)
return [
DefaultInfo(
files = depset([digest_file]),
),
K8sPushInfo(
image_label = ctx.label,
legacy_image_name = ctx.attr.image_name,
registry = registry,
repository = repository,
digestfile = digest_file,
),
]
external_image = rule(
implementation = _external_image_impl,
attrs = {
"image": attr.string(mandatory = True, doc = "The image location, e.g. gcr.io/foo/bar:baz"),
"image_name": attr.string(doc = "Image name, e.g. exernalserver. DEPRECATED: Use full target label instead, e.g. //images:externalserver"),
"digest": attr.string(mandatory = True, doc = "The image digest, e.g. sha256:deadbeef"),
},
)
================================================
FILE: adapters/providers.bzl
================================================
"""Provider definitions for container image adapters."""
K8sPushInfo = provider(
doc = "Information required to inject image into a manifest and optionally push it",
fields = {
"image_label": "bazel target label of the image",
"legacy_image_name": "Optional: short name",
"registry": "registry where the image resides",
"repository": "repository where the image resides",
"digestfile": "a file containing the digest of the image",
"pusher": "Optional: an executable target used to push the image to a remote registry",
"run_environment": "Optional: a run environment info provider used for pushing the image",
},
)
================================================
FILE: adapters/rules_img.bzl
================================================
"""Adapter for rules_img container images."""
load("@rules_img//img:providers.bzl", "DeployInfo", "ImageIndexInfo", "ImageManifestInfo")
load("//adapters:providers.bzl", "K8sPushInfo")
def _k8s_push_info_impl(ctx):
digestfile = ctx.attr.image[OutputGroupInfo].digest.to_list()[0]
return [
DefaultInfo(),
K8sPushInfo(
image_label = ctx.attr.image.label,
registry = ctx.attr.registry,
repository = ctx.attr.repository,
digestfile = digestfile,
pusher = ctx.attr.push[DefaultInfo],
run_environment = ctx.attr.push[RunEnvironmentInfo],
),
]
k8s_push_info = rule(
implementation = _k8s_push_info_impl,
attrs = {
"image": attr.label(mandatory = True, providers = [[ImageManifestInfo], [ImageIndexInfo]]),
"push": attr.label(mandatory = True, cfg = "target", providers = [DeployInfo]),
"registry": attr.string(mandatory = True),
"repository": attr.string(mandatory = True),
},
)
================================================
FILE: e2e/BUILD.bazel
================================================
load("//e2e/util:util.bzl", "e2e_test", "kubectl_cmd")
kubectl_cmd(
name = "verify_application",
args = ["-n \"$${USER}\" wait --timeout=180s --for=condition=Available deployment.apps/helloworld"],
)
e2e_test(
name = "manual_k8s",
steps = [
"//e2e/deployment:mynamespace.apply",
"//e2e:verify_application",
"//e2e/deployment:mynamespace.delete",
],
tags = [
"skip-bazel-ci",
],
)
================================================
FILE: e2e/deployment/BUILD.bazel
================================================
load("@rules_gitops//adapters:rules_img.bzl", "k8s_push_info")
load("@rules_gitops//gitops:defs.bzl", "k8s_deploy")
load("@rules_go//go:def.bzl", "go_binary", "go_library")
load("@rules_img//img:image.bzl", "image_manifest")
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:push.bzl", "image_push")
CLUSTER = "kind-kind"
USER = "kind-kind"
REGISTRY = "localhost:15000"
package(
default_visibility = ["//e2e:__subpackages__"],
)
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
go_library(
name = "go_default_library",
srcs = ["helloworld.go"],
importpath = "github.com/adobe/rules_gitops/e2e/deployment",
)
go_binary(
name = "helloworld",
embed = [":go_default_library"],
# Use pure Go (no CGO) to create a static binary compatible with Alpine (musl libc)
pure = "on",
)
image_layer(
name = "go_layer",
srcs = {
"/bin/app": ":helloworld",
},
)
image_manifest(
name = "go_image",
base = "@alpine",
entrypoint = ["/bin/app"],
layers = [
":go_layer",
],
platform = ":linux_amd64",
)
image_push(
name = "push",
image = ":go_image",
registry = REGISTRY,
repository = "rules_gitops/e2e/deployment/helloworld",
)
k8s_push_info(
name = "k8s_image",
image = ":go_image",
push = ":push",
registry = REGISTRY,
repository = "rules_gitops/e2e/deployment/helloworld",
)
k8s_deploy(
name = "mynamespace",
cluster = CLUSTER,
images = [":k8s_image"],
manifests = [
"manifests/deployment.yaml",
"manifests/service.yaml",
],
namespace = "{BUILD_USER}",
user = USER,
)
k8s_deploy(
name = "gitops",
cluster = CLUSTER,
deployment_branch = "e2e-deployment",
gitops = True,
images = [":k8s_image"],
manifests = [
"manifests/deployment.yaml",
"manifests/service.yaml",
],
namespace = "hwteam",
release_branch_prefix = "main",
user = USER,
)
================================================
FILE: e2e/deployment/helloworld.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
)
var (
port = flag.Int("port", 8080, "IP port")
)
func printenv(w http.ResponseWriter, r *http.Request) {
for _, e := range os.Environ() {
fmt.Fprintf(w, "%s\n", e)
}
}
func home(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello World!")
}
func main() {
flag.Parse()
http.HandleFunc("/", home)
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok\n")
})
http.HandleFunc("/env", printenv)
fmt.Printf("Serving on port %d\n", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
================================================
FILE: e2e/deployment/manifests/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
template:
metadata:
labels:
app: helloworld
spec:
containers:
- name: helloworld
image: //e2e/deployment:go_image
args:
- --port=8080
ports:
- containerPort: 8080
name: web
resources:
requests:
memory: 2Mi
readinessProbe:
httpGet:
path: /healthz
port: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
================================================
FILE: e2e/deployment/manifests/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: helloworld
labels:
app: helloworld
spec:
ports:
- port: 80
name: web
targetPort: 8080
selector:
app: helloworld
================================================
FILE: e2e/util/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
package(
default_visibility = ["//e2e:__subpackages__"],
)
exports_files(
[
"test.sh",
"kubectl_cmd.sh.tpl",
],
)
sh_binary(
name = "setup",
srcs = ["setup.sh"],
data = [
"//kubectl:resolved_toolchain",
"@io_k8s_sigs_kind//:kind",
],
env = {
"KIND_BIN_PATH": "$(rlocationpath @io_k8s_sigs_kind//:kind)",
"KUBECTL_BIN_PATH": "$(rlocationpath //kubectl:resolved_toolchain)",
},
toolchains = [
"//kubectl:resolved_toolchain",
],
deps = [
"@bazel_tools//tools/bash/runfiles",
],
)
sh_binary(
name = "teardown",
srcs = ["teardown.sh"],
data = [
"//kubectl:resolved_toolchain",
"@io_k8s_sigs_kind//:kind",
],
env = {
"KIND_BIN_PATH": "$(rlocationpath @io_k8s_sigs_kind//:kind)",
"KUBECTL_BIN_PATH": "$(rlocationpath //kubectl:resolved_toolchain)",
},
deps = [
"@bazel_tools//tools/bash/runfiles",
],
)
bzl_library(
name = "util",
srcs = ["util.bzl"],
deps = [
"@bazel_lib//lib:expand_template",
"@rules_shell//shell:rules_bzl",
],
)
================================================
FILE: e2e/util/kubectl_cmd.sh.tpl
================================================
#!/usr/bin/env bash
set -o errexit
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
KUBECTL_BIN=$(rlocation "${KUBECTL_BIN_PATH}")
if [[ ! -f "${KUBECTL_BIN}" ]]; then
echo >&2 "ERROR: could not find kubectl binary"
exit 1
fi
"${KUBECTL_BIN}" $$KUBECTL_ARGS$$
================================================
FILE: e2e/util/setup.sh
================================================
#!/usr/bin/env bash
set -o errexit
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
# desired cluster name; default is "kind"
KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}"
KIND_BIN=$(rlocation "${KIND_BIN_PATH}")
if [[ ! -f "${KIND_BIN}" ]]; then
echo >&2 "ERROR: could not find kind binary"
exit 1
fi
KUBECTL_BIN=$(rlocation "${KUBECTL_BIN_PATH}")
if [[ ! -f "${KUBECTL_BIN}" ]]; then
echo >&2 "ERROR: could not find kubectl binary"
exit 1
fi
DOCKER_BIN=$(which docker)
if [[ ! -f "${DOCKER_BIN}" ]]; then
echo >&2 "ERROR: could not find docker binary"
exit 1
fi
echo "Using docker: ${DOCKER_BIN}"
# create registry container unless it already exists
reg_name='kind-registry'
reg_port='15000'
running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)"
if [ "${running}" != 'true' ]; then
docker container rm "${reg_name}" 2>/dev/null || true
docker run \
-d --restart=always -e "REGISTRY_HTTP_ADDR=0.0.0.0:${reg_port}" -p "${reg_port}:${reg_port}" --name "${reg_name}" \
registry:3
fi
# create a cluster with the local registry enabled in containerd
cat </dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
DOCKER_BIN=$(which docker)
if [[ ! -f "${DOCKER_BIN}" ]]; then
echo >&2 "ERROR: could not find docker binary"
exit 1
fi
# desired cluster name; default is "kind"
KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}"
KIND_BIN=$(rlocation "${KIND_BIN_PATH}")
if [[ ! -f "${KIND_BIN}" ]]; then
echo >&2 "ERROR: could not find kind binary"
exit 1
fi
KUBECTL_BIN=$(rlocation "${KUBECTL_BIN_PATH}")
if [[ ! -f "${KUBECTL_BIN}" ]]; then
echo >&2 "ERROR: could not find kubectl binary"
exit 1
fi
"${KUBECTL_BIN}" config use-context kind-kind
MYNAMESPACE="${USER}"
echo "=== DEBUG: Cluster status ==="
echo "--- Nodes ---"
"${KUBECTL_BIN}" get nodes -o wide || true
echo "--- All pods in namespace ${MYNAMESPACE} ---"
"${KUBECTL_BIN}" get pods -n "${MYNAMESPACE}" -o wide || true
echo "--- Deployment status ---"
"${KUBECTL_BIN}" get deployments -n "${MYNAMESPACE}" -o wide || true
echo "--- Describe deployment helloworld ---"
"${KUBECTL_BIN}" describe deployment helloworld -n "${MYNAMESPACE}" || true
echo "--- Pod logs (if any) ---"
for pod in $("${KUBECTL_BIN}" get pods -n "${MYNAMESPACE}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
echo "=== Logs for pod: ${pod} ==="
"${KUBECTL_BIN}" logs "${pod}" -n "${MYNAMESPACE}" --tail=50 || true
echo "=== Events for pod: ${pod} ==="
"${KUBECTL_BIN}" describe pod "${pod}" -n "${MYNAMESPACE}" | grep -A 20 "^Events:" || true
done
echo "--- Events in namespace ${MYNAMESPACE} ---"
"${KUBECTL_BIN}" get events -n "${MYNAMESPACE}" --sort-by='.lastTimestamp' || true
echo "=== END DEBUG ==="
"${KIND_BIN}" delete cluster -n "${KIND_CLUSTER_NAME}" || true
echo "Deleting kind-registry"
"${DOCKER_BIN}" stop kind-registry || true
"${DOCKER_BIN}" container rm kind-registry || true
================================================
FILE: e2e/util/test.sh
================================================
#!/usr/bin/env bash
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
# Track overall exit code
exit_code=0
# Resolve teardown script path (needed for trap)
teardown_script=""
if [[ -n "${TEARDOWN:-}" ]]; then
teardown_script=$(rlocation "${TEARDOWN}")
if [[ -z "${teardown_script}" || ! -f "${teardown_script}" ]]; then
echo >&2 "ERROR: could not find TEARDOWN script at rlocationpath: ${TEARDOWN}"
exit 1
fi
fi
# Teardown runs on exit, regardless of success or failure
cleanup() {
if [[ -n "${teardown_script}" ]]; then
echo "=== Running teardown: ${TEARDOWN} ==="
"${teardown_script}" || true
echo "=== Teardown completed ==="
fi
}
trap cleanup EXIT
# Run setup if specified
if [[ -n "${SETUP:-}" ]]; then
setup_script=$(rlocation "${SETUP}")
if [[ -z "${setup_script}" || ! -f "${setup_script}" ]]; then
echo >&2 "ERROR: could not find SETUP script at rlocationpath: ${SETUP}"
exit 1
fi
echo "=== Running setup: ${SETUP} ==="
if ! "${setup_script}"; then
echo >&2 "=== Setup failed ==="
exit 1
fi
echo "=== Setup completed ==="
fi
# Run all scripts passed as arguments
for rlocation_path in "$@"; do
script=$(rlocation "${rlocation_path}")
if [[ -z "${script}" || ! -f "${script}" ]]; then
echo >&2 "ERROR: could not find script at rlocationpath: ${rlocation_path}"
exit_code=1
break
fi
echo "=== Running: ${rlocation_path} ==="
if ! "${script}"; then
echo >&2 "=== Failed: ${rlocation_path} ==="
exit_code=1
break
fi
echo "=== Completed: ${rlocation_path} ==="
done
if [[ ${exit_code} -eq 0 ]]; then
echo "=== All scripts completed successfully ==="
fi
exit ${exit_code}
================================================
FILE: e2e/util/util.bzl
================================================
"""Utility macros for e2e testing with kind clusters."""
load("@bazel_lib//lib:expand_template.bzl", "expand_template")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
def e2e_test(name, steps, **kwargs):
"""Declares an e2e test interacting with a kind cluster
Args:
name: name of the test target
steps: a list of labels producing binaries to run
**kwargs: additional options to pass to the underlying test rule
"""
sh_test(
name = "manual_k8s",
srcs = ["//e2e/util:test.sh"],
args = ["$(rlocationpath {label})".format(label = label) for label in steps],
data = [
"//e2e/util:setup.sh",
"@io_k8s_sigs_kind//:kind",
"//kubectl:resolved_toolchain",
"//e2e/util:teardown.sh",
] + steps,
env = {
"SETUP": "$(rlocationpath //e2e/util:setup.sh)",
"TEARDOWN": "$(rlocationpath //e2e/util:teardown.sh)",
"KIND_BIN_PATH": "$(rlocationpath @io_k8s_sigs_kind//:kind)",
"KUBECTL_BIN_PATH": "$(rlocationpath //kubectl:resolved_toolchain)",
},
deps = [
"@bazel_tools//tools/bash/runfiles",
],
**kwargs
)
def kubectl_cmd(name, args):
expand_template(
name = name + ".script",
is_executable = True,
template = "//e2e/util:kubectl_cmd.sh.tpl",
substitutions = {
"$$KUBECTL_ARGS$$": " ".join(args),
},
out = name + ".bash",
)
sh_binary(
name = name,
srcs = [name + ".script"],
env = {
"KUBECTL_BIN_PATH": "$(rlocationpath //kubectl:resolved_toolchain)",
},
data = [
"//kubectl:resolved_toolchain",
],
deps = [
"@bazel_tools//tools/bash/runfiles",
],
toolchains = [
"//kubectl:resolved_toolchain",
],
)
================================================
FILE: examples/README.md
================================================
# Bazel GitOps Rules Examples
## Overview
Examples projects to demonstrate Bazel GitOps Rules in use:
- [helloworld](./helloworld) -- a minimal Go application with `k8s_deploy` manifests
- [legacy_docker](./legacy_docker/) -- a minimal container image built with rules_docker and adapted for use with rules_gitops
================================================
FILE: examples/helloworld/.bazelrc
================================================
common --lockfile_mode=off
================================================
FILE: examples/helloworld/.bazelversion
================================================
7.6.0
================================================
FILE: examples/helloworld/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_gitops//adapters:rules_img.bzl", "k8s_push_info")
load("@rules_gitops//gitops:defs.bzl", "k8s_deploy")
load("@rules_gitops//kustomize:defs.bzl", "kustomization")
load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
load("@rules_img//img:image.bzl", "image_index", "image_manifest")
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:load.bzl", "image_load")
load("@rules_img//img:push.bzl", "image_push")
CLUSTER = "kind-kind"
USER = "kind-kind"
REGISTRY = "localhost:15000"
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
platform(
name = "linux_arm64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:aarch64",
],
)
go_library(
name = "go_default_library",
srcs = ["helloworld.go"],
importpath = "github.com/adobe/rules_gitops/examples/helloworld",
visibility = ["//visibility:private"],
)
go_test(
name = "go_default_test",
srcs = ["helloworld_test.go"],
embed = [":go_default_library"],
)
go_binary(
name = "helloworld",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
image_layer(
name = "go_layer",
srcs = {
"/bin/app": ":helloworld",
},
)
image_manifest(
name = "go_image",
base = "@alpine",
entrypoint = ["/bin/app"],
layers = [
":go_layer",
],
platform = ":linux_amd64",
)
image_index(
name = "multiarch_image",
manifests = [":go_image"],
platforms = [
":linux_amd64",
":linux_arm64",
],
)
image_load(
name = "load",
image = ":go_image",
tag = "helloworld:latest",
visibility = ["//:__pkg__"],
)
image_push(
name = "push",
image = ":go_image",
registry = REGISTRY,
repository = "rules_gitops/examples/helloworld",
tag = "native",
visibility = ["//:__pkg__"],
)
image_push(
name = "push_multiarch",
image = ":multiarch_image",
registry = REGISTRY,
repository = "rules_gitops/examples/helloworld",
tag = "native",
visibility = ["//:__pkg__"],
)
k8s_push_info(
name = "k8s_image",
image = ":go_image",
push = ":push",
registry = REGISTRY,
repository = "rules_gitops/examples/helloworld",
)
k8s_push_info(
name = "k8s_image_multiarch",
image = ":multiarch_image",
push = ":push_multiarch",
registry = REGISTRY,
repository = "rules_gitops/examples/helloworld",
)
kustomization(
name = "kustomize",
testonly = True,
images = [":k8s_image"],
manifests = [
"deployment.yaml",
],
namespace = "",
)
k8s_deploy(
name = "mynamespace",
cluster = CLUSTER,
images = [":k8s_image"],
manifests = [
"deployment.yaml",
"service.yaml",
],
namespace = "{BUILD_USER}",
user = USER,
)
# NAMESPACE = "hwteam"
# k8s_deploy(
# name = "canary",
# cluster = CLUSTER,
# deployment_branch = "helloworld-canary",
# image_digest_tag = True, # test optional image tagging
# image_registry = REGISTRY, # override the default registry for production
# image_repository_prefix = "k8s",
# images = {
# "helloworld-image": ":image",
# },
# manifests = [
# "deployment.yaml",
# "service.yaml",
# ],
# name_suffix = "-canary",
# namespace = NAMESPACE,
# prefix_suffix_app_labels = True,
# user = USER,
# )
# k8s_deploy(
# name = "release",
# cluster = CLUSTER,
# deployment_branch = "helloworld-prod",
# image_digest_tag = True, # test optional image tagging
# image_registry = REGISTRY, # override the default registry host for production
# image_repository_prefix = "k8s",
# images = {
# "helloworld-image": ":image",
# },
# manifests = [
# "deployment.yaml",
# "service.yaml",
# ],
# namespace = NAMESPACE,
# tags = ["release"],
# user = USER,
# )
# k8s_deploy(
# name = "gitops_custom_path",
# cluster = CLUSTER,
# deployment_branch = "helloworld-gitops-custom-path",
# gitops_path = "custom_cloud",
# image_digest_tag = True, # test optional image tagging
# image_registry = REGISTRY, # override the default registry host for production
# image_repository_prefix = "k8s",
# images = {
# "helloworld-image": ":image",
# },
# manifests = [
# "deployment.yaml",
# "service.yaml",
# ],
# name_suffix = "-gitops-custom-path",
# namespace = NAMESPACE,
# user = USER,
# )
# sh_test(
# name = "k8s_deploy_test",
# srcs = ["k8s_deploy_test.sh"],
# args = [
# CLUSTER,
# NAMESPACE,
# ],
# data = [
# ":canary.show",
# ":mynamespace.show",
# ":release.show",
# ],
# deps = [
# "@bazel_tools//tools/bash/runfiles",
# ],
# )
================================================
FILE: examples/helloworld/MODULE.bazel
================================================
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "rules_gitops", dev_dependency = True)
kustomize = use_extension("@rules_gitops//kustomize:defs.bzl", "kustomize")
kustomize.toolchain(
version = "5.7.1",
)
local_path_override(
module_name = "rules_gitops",
path = "../..",
)
bazel_dep(name = "rules_go", version = "0.52.0")
bazel_dep(name = "rules_img", version = "0.3.3", dev_dependency = True)
pull = use_repo_rule("@rules_img//img:pull.bzl", "pull")
pull(
name = "alpine",
digest = "sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375",
layer_handling = "lazy",
registries = [
"mirror.gcr.io",
"index.docker.io",
],
repository = "library/alpine",
tag = "3.23",
)
================================================
FILE: examples/helloworld/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
template:
metadata:
labels:
app: helloworld
spec:
containers:
- name: helloworld
image: //:go_image
args:
- --port=8080
ports:
- containerPort: 8080
name: web
resources:
requests:
memory: 2Mi
readinessProbe:
httpGet:
path: /healthz
port: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
================================================
FILE: examples/helloworld/helloworld.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
)
var (
port = flag.Int("port", 8080, "IP port")
)
func printenv(w http.ResponseWriter, r *http.Request) {
for _, e := range os.Environ() {
fmt.Fprintf(w, "%s\n", e)
}
}
func home(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello World!")
}
func main() {
flag.Parse()
http.HandleFunc("/", home)
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok\n")
})
http.HandleFunc("/env", printenv)
fmt.Printf("Serving on port %d\n", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
================================================
FILE: examples/helloworld/helloworld_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"io"
"net/http/httptest"
"strings"
"testing"
)
func TestHome(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
w := httptest.NewRecorder()
home(w, req)
resp := w.Result()
if resp.StatusCode != 200 {
t.Fatalf("Unexpected status code %d, expectted 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "Hello World") {
t.Error("Unexpected content returned:", string(body))
}
}
================================================
FILE: examples/helloworld/k8s_deploy_test.sh
================================================
#!/usr/bin/env bash
# Debug
# set -x
# RUNFILES_LIB_DEBUG=1
# --- begin runfiles.bash initialization v2 ---
# Copy-pasted from the Bazel Bash runfiles library v2.
set -uo pipefail
f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null ||
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null ||
source "$0.runfiles/$f" 2>/dev/null ||
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null ||
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null ||
{
echo >&2 "ERROR: cannot find $f"
exit 1
}
f=
set -e
# --- end runfiles.bash initialization v2 ---
CLUSTER="$1"
NAMESPACE="$2"
$(rlocation examples/helloworld/mynamespace.show) > mynamespace.show
echo "DEBUG: mynamespace.show:"
cat mynamespace.show
grep -F "kind: Deployment" mynamespace.show
grep -F "kind: Service" mynamespace.show
grep -F "name: helloworld" mynamespace.show
grep -E "image: localhost:15000/.*/helloworld/image@sha256" mynamespace.show
grep -E "app_label_image_short_digest" mynamespace.show | grep -v -F 'image.short-digest'
$(rlocation examples/helloworld/canary.show) > canary.show
echo "DEBUG: canary.show:"
cat canary.show
grep -F "kind: Deployment" canary.show
grep -F "kind: Service" canary.show
grep -F "namespace: $NAMESPACE" canary.show
grep -F "name: helloworld-canary" canary.show
grep -E "image: localhost:15000/k8s/helloworld/image@sha256" canary.show
$(rlocation examples/helloworld/release.show) > release.show
echo "DEBUG: release.show:"
cat release.show
grep -F "kind: Deployment" release.show
grep -F "kind: Service" release.show
grep -F "namespace: $NAMESPACE" canary.show
grep -F "name: helloworld" mynamespace.show
grep -E "image: localhost:15000/k8s/helloworld/image@sha256" release.show
================================================
FILE: examples/helloworld/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: helloworld
labels:
app: helloworld
spec:
ports:
- port: 80
name: web
targetPort: 8080
selector:
app: helloworld
================================================
FILE: examples/legacy_docker/.bazelrc
================================================
common --lockfile_mode=off
common --@io_bazel_rules_docker//transitions:enable=false
================================================
FILE: examples/legacy_docker/.bazelversion
================================================
7.6.0
================================================
FILE: examples/legacy_docker/BUILD.bazel
================================================
load(
"@io_bazel_rules_docker//container:container.bzl",
"container_image",
"container_push",
)
load(
"@rules_gitops//gitops:defs.bzl",
"k8s_deploy",
)
load(
"//adapters:docker.bzl",
"k8s_push_info",
)
CLUSTER = "kind-kind"
USER = "kind-kind"
REGISTRY = "localhost:15000"
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
container_image(
name = "image",
base = "@alpine_linux_amd64//image",
cmd = [
"/bin/sleep",
"10",
],
files = [":container_contents.txt"],
)
genrule(
name = "image_digestsha256",
srcs = [":image.digest"],
outs = ["image.digestsha256"],
cmd = "sed 's/sha256://' $< > $@",
)
container_push(
name = "push",
format = "Docker",
image = "image",
registry = REGISTRY,
repository = "rules_gitops/examples/legacy_docker/image",
tag_file = "image.digestsha256",
)
k8s_push_info(
name = "k8s_image",
image = "image",
push = "push",
registry = REGISTRY,
repository = "rules_gitops/examples/legacy_docker/image",
)
k8s_deploy(
name = "mynamespace",
cluster = CLUSTER,
gitops = True,
images = [":k8s_image"],
manifests = [
"deployment.yaml",
],
namespace = "hwteam",
user = USER,
)
================================================
FILE: examples/legacy_docker/MODULE.bazel
================================================
bazel_dep(name = "rules_gitops", dev_dependency = True)
kustomize = use_extension("@rules_gitops//kustomize:defs.bzl", "kustomize")
kustomize.toolchain(
version = "5.7.1",
)
local_path_override(
module_name = "rules_gitops",
path = "../..",
)
================================================
FILE: examples/legacy_docker/WORKSPACE
================================================
workspace(
name = "hzrepo",
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
###############################
# Golang
###############################
http_archive(
name = "io_bazel_rules_go",
sha256 = "0936c9bc3c4321ee372cb8f66dd972d368cb940ed01a9ba9fd7debcf0093f09b",
urls = [
"https://github.com/bazel-contrib/rules_go/releases/download/v0.51.0/rules_go-v0.51.0.zip",
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.51.0/rules_go-v0.51.0.zip",
],
)
load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_register_toolchains", "go_rules_dependencies")
# Download Go SDK
go_download_sdk(
name = "go_sdk",
version = "1.23.4",
)
go_rules_dependencies()
go_register_toolchains()
http_archive(
name = "bazel_gazelle",
integrity = "sha256-qAiTKSrh146u7dUNHKuY8kKhfj1XQbG5+1i1/Z0tV7w=",
urls = [
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.40.0/bazel-gazelle-v0.40.0.tar.gz",
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.40.0/bazel-gazelle-v0.40.0.tar.gz",
],
)
###############################
# rules_docker
###############################
http_archive(
name = "io_bazel_rules_docker",
patch_args = ["-p1"],
# https://github.com/bazelbuild/rules_docker/pull/2068#issuecomment-1242158255
patches = ["//:patches/docker.patch"],
sha256 = "f6dcb97e992f13bc9effd794e9bb300f06b0dadc88061f81ae68d8d5994be964",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_docker/releases/download/v0.26.0/rules_docker-v0.26.0.tar.gz",
"https://github.com/bazelbuild/rules_docker/releases/download/v0.26.0/rules_docker-v0.26.0.tar.gz",
],
)
load("@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories")
container_repositories()
load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
container_deps()
load(
"@io_bazel_rules_docker//container:container.bzl",
"container_pull",
)
container_pull(
name = "alpine_linux_amd64",
digest = "sha256:954b378c375d852eb3c63ab88978f640b4348b01c1b3456a024a81536dafbbf4",
registry = "index.docker.io",
repository = "library/alpine",
tag = "3.8",
)
================================================
FILE: examples/legacy_docker/adapters/BUILD.bazel
================================================
================================================
FILE: examples/legacy_docker/adapters/docker.bzl
================================================
"""Adapter for legacy io_bazel_rules_docker container images."""
load("@io_bazel_rules_docker//container:providers.bzl", "ImageInfo", "PushInfo")
load("@rules_gitops//adapters:providers.bzl", "K8sPushInfo")
def _k8s_push_info_impl(ctx):
digestfile = ctx.attr.image[ImageInfo].container_parts["digest"]
return [
DefaultInfo(),
K8sPushInfo(
image_label = ctx.attr.image.label,
registry = ctx.attr.registry,
repository = ctx.attr.repository,
digestfile = digestfile,
pusher = ctx.attr.push[DefaultInfo],
),
]
k8s_push_info = rule(
implementation = _k8s_push_info_impl,
attrs = {
"image": attr.label(mandatory = True, providers = [ImageInfo]),
"push": attr.label(mandatory = True, cfg = "target", providers = [PushInfo]),
"registry": attr.string(mandatory = True),
"repository": attr.string(mandatory = True),
},
)
================================================
FILE: examples/legacy_docker/container_contents.txt
================================================
Hello rules_docker!
================================================
FILE: examples/legacy_docker/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacydocker
spec:
replicas: 1
selector:
matchLabels:
app: legacydocker
template:
metadata:
labels:
app: legacydocker
spec:
containers:
- name: legacydocker
image: //:image
================================================
FILE: examples/legacy_docker/patches/docker.patch
================================================
diff --git a/container/image.bzl b/container/image.bzl
index ceeea01..f2762d4 100644
--- a/container/image.bzl
+++ b/container/image.bzl
@@ -769,11 +769,7 @@ _outputs["build_script"] = "%{name}.executable"
def _image_transition_impl(settings, attr):
if not settings["@io_bazel_rules_docker//transitions:enable"]:
# Once bazel < 5.0 is not supported we can return an empty dict here
- return {
- "//command_line_option:platforms": settings["//command_line_option:platforms"],
- "@io_bazel_rules_docker//platforms:image_transition_cpu": "//plaftorms:image_transition_cpu_unset",
- "@io_bazel_rules_docker//platforms:image_transition_os": "//plaftorms:image_transition_os_unset",
- }
+ return {}
return {
"//command_line_option:platforms": "@io_bazel_rules_docker//platforms:image_transition",
================================================
FILE: gitops/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
licenses(["notice"]) # Apache 2.0
package(default_visibility = ["//visibility:public"])
alias(
name = "nameprefix_deployment_labels_config.yaml",
actual = "//gitops/private:nameprefix_deployment_labels_config.yaml",
)
alias(
name = "namesuffix_deployment_labels_config.yaml",
actual = "//gitops/private:namesuffix_deployment_labels_config.yaml",
)
bzl_library(
name = "defs",
srcs = ["defs.bzl"],
deps = [
"//gitops/private:gitops",
"//gitops/private:k8s_deploy",
],
)
================================================
FILE: gitops/analysis/BUILD.bazel
================================================
load("@protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_go//go:def.bzl", "go_library")
load("@rules_go//proto:def.bzl", "go_proto_library")
go_library(
name = "go_default_library",
srcs = ["analysis.go"],
embed = [":analysis_go_proto"],
importpath = "github.com/adobe/rules_gitops/gitops/analysis",
visibility = ["//visibility:public"],
)
proto_library(
name = "analysis_proto",
srcs = ["analysis.proto"],
visibility = ["//visibility:public"],
deps = ["//gitops/blaze_query:blaze_query_proto"],
)
go_proto_library(
name = "analysis_go_proto",
importpath = "github.com/adobe/rules_gitops/gitops/analysis",
proto = ":analysis_proto",
visibility = ["//visibility:public"],
deps = ["//gitops/blaze_query:go_default_library"],
)
================================================
FILE: gitops/analysis/analysis.go
================================================
package analysis
================================================
FILE: gitops/analysis/analysis.proto
================================================
// Copyright 2018 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package analysis;
option java_package = "com.google.devtools.build.lib.analysis";
option java_outer_classname = "AnalysisProtos";
import "gitops/blaze_query/build.proto";
// Container for the action graph properties.
message ActionGraphContainer {
repeated Artifact artifacts = 1;
repeated Action actions = 2;
repeated Target targets = 3;
repeated DepSetOfFiles dep_set_of_files = 4;
repeated Configuration configuration = 5;
repeated AspectDescriptor aspect_descriptors = 6;
repeated RuleClass rule_classes = 7;
}
// Represents a single artifact, whether it's a source file or a derived output
// file.
message Artifact {
// Identifier for this artifact; this is an opaque string, only valid for this
// particular dump of the analysis.
string id = 1;
// The relative path of the file within the execution root.
string exec_path = 2;
// True iff the artifact is a tree artifact, i.e. the above exec_path refers
// a directory.
bool is_tree_artifact = 3;
}
// Represents a single action, which is a function from Artifact(s) to
// Artifact(s).
message Action {
// The target that was responsible for the creation of the action.
string target_id = 1;
// The aspects that were responsible for the creation of the action (if any).
// In the case of aspect-on-aspect, AspectDescriptors are listed in
// topological order of the dependency graph.
// e.g. [A, B] would imply that aspect A is applied on top of aspect B.
repeated string aspect_descriptor_ids = 2;
// Encodes all significant behavior that might affect the output. The key
// must change if the work performed by the execution of this action changes.
// Note that the key doesn't include checksums of the input files.
string action_key = 3;
// The mnemonic for this kind of action.
string mnemonic = 4;
// The configuration under which this action is executed.
string configuration_id = 5;
// The command line arguments of the action. This will be only set if
// explicitly requested.
repeated string arguments = 6;
// The list of environment variables to be set before executing the command.
repeated KeyValuePair environment_variables = 7;
// The set of input dep sets that the action depends upon. If the action does
// input discovery, the contents of this set might change during execution.
repeated string input_dep_set_ids = 8;
// The list of Artifact IDs that represent the output files that this action
// will generate.
repeated string output_ids = 9;
// True iff the action does input discovery during execution.
bool discovers_inputs = 10;
// Execution info for the action. Remote execution services may use this
// information to modify the execution environment, but actions will
// generally not be aware of it.
repeated KeyValuePair execution_info = 11;
// The list of param files. This will be only set if explicitly requested.
repeated ParamFile param_files = 12;
}
// Represents a single target (without configuration information) that is
// associated with an action.
message Target {
// Identifier for this target; this is an opaque string, only valid for this
// particular dump of the analysis.
string id = 1;
// Label of the target, e.g. //foo:bar.
string label = 2;
// Class of the rule.
string rule_class_id = 3;
}
message RuleClass {
// Identifier for this rule class; this is an opaque string, only valid for
// this particular dump of the analysis.
string id = 1;
// Name of the rule class, e.g. cc_library.
string name = 2;
}
// Represents an invocation specific descriptor of an aspect.
message AspectDescriptor {
// Identifier for this aspect descriptor; this is an opaque string, only valid
// for the particular dump of the analysis.
string id = 1;
// The name of the corresponding aspect. For native aspects, it's the Java
// class name, for Skylark aspects it's the bzl file followed by a % sign
// followed by the name of the aspect.
string name = 2;
// The list of parameters bound to a particular invocation of that aspect on
// a target. Note that aspects can be executed multiple times on the same
// target in different order.
repeated KeyValuePair parameters = 3;
}
message DepSetOfFiles {
// Identifier for this named set of files; this is an opaque string, only
// valid for the particular dump of the analysis.
string id = 1;
// Other transitively included named set of files.
repeated string transitive_dep_set_ids = 2;
// The list of input artifact IDs that are immediately contained in this set.
repeated string direct_artifact_ids = 3;
}
message Configuration {
// Identifier for this configuration; this is an opaque string, only valid for
// the particular dump of the analysis.
string id = 1;
// The mnemonic representing the build configuration.
string mnemonic = 2;
// The platform string.
string platform_name = 3;
// The checksum representation of the configuration options;
string checksum = 4;
}
message KeyValuePair {
// The variable name.
string key = 1;
// The variable value.
string value = 2;
}
message ConfiguredTarget {
// The target. We use blaze_query.Target defined in build.proto instead of
// the Target defined in this file because blaze_query.Target is much heavier
// and will output proto results similar to what users are familiar with from
// regular blaze query.
blaze_query.Target target = 1;
// The configuration
Configuration configuration = 2;
}
// Container for cquery results
message CqueryResult {
// All the configuredtargets returns by cquery
repeated ConfiguredTarget results = 1;
}
// Content of a param file.
message ParamFile {
// The exec path of the param file artifact.
string exec_path = 1;
// The arguments in the param file.
// Each argument corresponds to a line in the param file.
repeated string arguments = 2;
}
================================================
FILE: gitops/bazel/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library", "go_test")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["bazeltargets.go"],
importpath = "github.com/adobe/rules_gitops/gitops/bazel",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["bazeltargets_test.go"],
embed = [":go_default_library"],
)
================================================
FILE: gitops/bazel/bazeltargets.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package bazel
import "strings"
// TargetToExecutable converts bazel target name to respective executable name in bazel-bin
func TargetToExecutable(target string) string {
if !strings.HasPrefix(target, "//") {
return target
}
target = "bazel-bin/" + target[2:]
target = strings.Replace(target, ":", "/", 1)
return target
}
================================================
FILE: gitops/bazel/bazeltargets_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package bazel
import "testing"
func TestTargetToExecutableHappypath(t *testing.T) {
s := TargetToExecutable("//rtb/bidder:rtb-uat-k8s01-iad-1b-bidder-first-uat.gitops")
if s != "bazel-bin/rtb/bidder/rtb-uat-k8s01-iad-1b-bidder-first-uat.gitops" {
t.Error("unexpected result", s)
}
}
================================================
FILE: gitops/blaze_query/BUILD.bazel
================================================
load("@protobuf//bazel:proto_library.bzl", "proto_library")
load("@rules_go//proto:def.bzl", "go_proto_library")
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
embed = [":blaze_query_go_proto"],
importpath = "github.com/adobe/rules_gitops/gitops/blaze_query",
visibility = ["//visibility:public"],
)
proto_library(
name = "blaze_query_proto",
srcs = ["build.proto"],
visibility = ["//visibility:public"],
)
go_proto_library(
name = "blaze_query_go_proto",
importpath = "github.com/adobe/rules_gitops/gitops/blaze_query",
proto = ":blaze_query_proto",
visibility = ["//visibility:public"],
)
================================================
FILE: gitops/blaze_query/build.proto
================================================
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file contains the protocol buffer representation of a build
// file or 'blaze query --output=proto' call.
syntax = "proto2";
package blaze_query;
// option cc_api_version = 2;
// option java_api_version = 1;
option java_package = "com.google.devtools.build.lib.query2.proto.proto2api";
message License {
repeated string license_type = 1;
repeated string exception = 2;
}
message StringDictEntry {
required string key = 1;
required string value = 2;
}
message LabelDictUnaryEntry {
required string key = 1;
required string value = 2;
}
message LabelListDictEntry {
required string key = 1;
repeated string value = 2;
}
message LabelKeyedStringDictEntry {
required string key = 1;
required string value = 2;
}
message StringListDictEntry {
required string key = 1;
repeated string value = 2;
}
// Represents an entry attribute of a Fileset rule in a build file.
message FilesetEntry {
// Indicates what to do when a source file is actually a symlink.
enum SymlinkBehavior {
COPY = 1;
DEREFERENCE = 2;
}
// The label pointing to the source target where files are copied from.
required string source = 1;
// The relative path within the fileset rule where files will be mapped.
required string destination_directory = 2;
// Whether the files= attribute was specified. This is necessary because
// no files= attribute and files=[] mean different things.
optional bool files_present = 7;
// A list of file labels to include from the source directory.
repeated string file = 3;
// If this is a fileset entry representing files within the rule
// package, this lists relative paths to files that should be excluded from
// the set. This cannot contain values if 'file' also has values.
repeated string exclude = 4;
// This field is optional because there will be some time when the new
// PB is used by tools depending on blaze query, but the new blaze version
// is not yet released.
// TODO(bazel-team): Make this field required once a version of Blaze is
// released that outputs this field.
optional SymlinkBehavior symlink_behavior = 5 [ default=COPY ];
// The prefix to strip from the path of the files in this FilesetEntry. Note
// that no value and the empty string as the value mean different things here.
optional string strip_prefix = 6;
}
// A rule attribute. Each attribute must have a type and one of the various
// value fields populated - for the most part.
//
// Attributes of BOOLEAN and TRISTATE type may set all of the int, bool, and
// string values for backwards compatibility with clients that expect them to
// be set.
//
// Attributes of INTEGER, STRING, LABEL, LICENSE, BOOLEAN, and TRISTATE type
// may set *none* of the values. This can happen if the Attribute message is
// prepared for a client that doesn't support SELECTOR_LIST, but the rule has
// a selector list value for the attribute. (Selector lists for attributes of
// other types--the collection types--are handled differently when prepared
// for such a client. The possible collection values are gathered together
// and flattened.)
//
// By checking the type, the appropriate value can be extracted - see the
// comments on each type for the associated value. The order of lists comes
// from the blaze parsing. If an attribute is of a list type, the associated
// list should never be empty.
message Attribute {
reserved 12, 16;
// Indicates the type of attribute.
enum Discriminator {
INTEGER = 1; // int_value
STRING = 2; // string_value
LABEL = 3; // string_value
OUTPUT = 4; // string_value
STRING_LIST = 5; // string_list_value
LABEL_LIST = 6; // string_list_value
OUTPUT_LIST = 7; // string_list_value
DISTRIBUTION_SET = 8; // string_list_value - order is unimportant
LICENSE = 9; // license
STRING_DICT = 10; // string_dict_value
FILESET_ENTRY_LIST = 11; // fileset_list_value
LABEL_LIST_DICT = 12; // label_list_dict_value
STRING_LIST_DICT = 13; // string_list_dict_value
BOOLEAN = 14; // int, bool and string value
TRISTATE = 15; // tristate, int and string value
INTEGER_LIST = 16; // int_list_value
UNKNOWN = 18; // unknown type, use only for build extensions
LABEL_DICT_UNARY = 19; // label_dict_unary_value
SELECTOR_LIST = 20; // selector_list
LABEL_KEYED_STRING_DICT = 21; // label_keyed_string_dict
DEPRECATED_STRING_DICT_UNARY = 17;
}
// Values for the TriState field type.
enum Tristate {
NO = 0;
YES = 1;
AUTO = 2;
}
message SelectorEntry {
reserved 12;
// The key of the selector entry. At this time, this is the label of a
// config_setting rule, or the pseudo-label "//conditions:default".
optional string label = 1;
// True if the entry's value is the default value for the type as a
// result of the condition value being specified as None (ie:
// {"//condition": None}).
optional bool is_default_value = 16;
// Exactly one of the following fields (except for glob_criteria) must be
// populated - note that the BOOLEAN and TRISTATE caveat in Attribute's
// comment does not apply here. The type field in the SelectorList
// containing this entry indicates which of these fields is populated,
// in accordance with the comments on Discriminator enum values above.
// (To be explicit: BOOLEAN populates the boolean_value field and TRISTATE
// populates the tristate_value field.)
optional int32 int_value = 2;
optional string string_value = 3;
optional bool boolean_value = 4;
optional Tristate tristate_value = 5;
repeated string string_list_value = 6;
optional License license = 7;
repeated StringDictEntry string_dict_value = 8;
repeated FilesetEntry fileset_list_value = 9;
repeated LabelListDictEntry label_list_dict_value = 10;
repeated StringListDictEntry string_list_dict_value = 11;
repeated int32 int_list_value = 13;
repeated LabelDictUnaryEntry label_dict_unary_value = 15;
repeated LabelKeyedStringDictEntry label_keyed_string_dict_value = 17;
repeated bytes DEPRECATED_string_dict_unary_value = 14;
}
message Selector {
// The list of (label, value) pairs in the map that defines the selector.
// At this time, this cannot be empty, i.e. a selector has at least one
// entry.
repeated SelectorEntry entries = 1;
// Whether or not this has any default values.
optional bool has_default_value = 2;
// The error message when no condition matches.
optional string no_match_error = 3;
}
message SelectorList {
// The type that this selector list evaluates to, and the type that each
// selector in the list evaluates to. At this time, this cannot be
// SELECTOR_LIST, i.e. selector lists do not nest.
optional Discriminator type = 1;
// The list of selector elements in this selector list. At this time, this
// cannot be empty, i.e. a selector list is never empty.
repeated Selector elements = 2;
}
// The name of the attribute
required string name = 1;
// Whether the attribute was explicitly specified
optional bool explicitly_specified = 13;
// If this attribute has a string value or a string list value, then this
// may be set to indicate that the value may be treated as a label that
// isn't a dependency of this attribute's rule.
optional bool nodep = 20;
// The type of attribute. This message is used for all of the different
// attribute types so the discriminator helps for figuring out what is
// stored in the message.
required Discriminator type = 2;
// If this attribute has an integer value this will be populated.
// Boolean and TriState also use this field as [0,1] and [-1,0,1]
// for [false, true] and [auto, no, yes] respectively.
optional int32 int_value = 3;
// If the attribute has a string value this will be populated. Label and
// path attributes use this field as the value even though the type may
// be LABEL or something else other than STRING.
optional string string_value = 5;
// If the attribute has a boolean value this will be populated.
optional bool boolean_value = 14;
// If the attribute is a Tristate value, this will be populated.
optional Tristate tristate_value = 15;
// The value of the attribute has a list of string values (label and path
// note from STRING applies here as well).
repeated string string_list_value = 6;
// If this is a license attribute, the license information is stored here.
optional License license = 7;
// If this is a string dict, each entry will be stored here.
repeated StringDictEntry string_dict_value = 8;
// If the attribute is part of a Fileset, the fileset entries are stored in
// this field.
repeated FilesetEntry fileset_list_value = 9;
// If this is a label list dict, each entry will be stored here.
repeated LabelListDictEntry label_list_dict_value = 10;
// If this is a string list dict, each entry will be stored here.
repeated StringListDictEntry string_list_dict_value = 11;
// The value of the attribute has a list of int32 values
repeated int32 int_list_value = 17;
// If this is a label dict unary, each entry will be stored here.
repeated LabelDictUnaryEntry label_dict_unary_value = 19;
// If this is a label-keyed string dict, each entry will be stored here.
repeated LabelKeyedStringDictEntry label_keyed_string_dict_value = 22;
// If this attribute's value is an expression containing one or more select
// expressions, then its type is SELECTOR_LIST and a SelectorList will be
// stored here.
optional SelectorList selector_list = 21;
repeated bytes DEPRECATED_string_dict_unary_value = 18;
}
// A rule instance (e.g., cc_library foo, java_binary bar).
message Rule {
reserved 8, 11;
// The name of the rule (formatted as an absolute label, e.g. //foo/bar:baz).
required string name = 1;
// The rule class (e.g., java_library)
required string rule_class = 2;
// The BUILD file and line number of the location (formatted as
// :) in the rule's package's BUILD file where the
// rule instance was instantiated. The line number will be that of a rule
// invocation or macro call (that in turn invoked a rule). See
// https://docs.bazel.build/versions/master/skylark/macros.html#macro-creation
optional string location = 3;
// All of the attributes that describe the rule.
repeated Attribute attribute = 4;
// All of the inputs to the rule (formatted as absolute labels). These are
// predecessors in the dependency graph.
repeated string rule_input = 5;
// All of the outputs of the rule (formatted as absolute labels). These are
// successors in the dependency graph.
repeated string rule_output = 6;
// The set of all "features" inherited from the rule's package declaration.
repeated string default_setting = 7;
// The rule's class's public by default value.
optional bool public_by_default = 9;
// If this rule is of a skylark-defined RuleClass.
optional bool is_skylark = 10;
// Hash encapsulating the behavior of this Skylark rule. Any change to this
// rule's definition that could change its behavior will be reflected here.
optional string skylark_environment_hash_code = 12;
}
// Summary of all transitive dependencies of 'rule,' where each dependent
// rule is included only once in the 'dependency' field. Gives complete
// information to analyze the single build target labeled rule.name,
// including optional location of target in BUILD file.
message RuleSummary {
required Rule rule = 1;
repeated Rule dependency = 2;
optional string location = 3;
}
// A package group. Aside from the name, it contains the list of packages
// present in the group (as specified in the BUILD file).
message PackageGroup {
reserved 4;
// The name of the package group
required string name = 1;
// The list of packages as specified in the BUILD file. Currently this is
// only a list of packages, but some time in the future, there might be
// some type of wildcard mechanism.
repeated string contained_package = 2;
// The list of sub package groups included in this one.
repeated string included_package_group = 3;
}
// An environment group.
message EnvironmentGroup {
// The name of the environment group.
required string name = 1;
// The environments that belong to this group (as labels).
repeated string environment = 2;
// The member environments that rules implicitly support if not otherwise
// specified.
repeated string default = 3;
}
// A file that is an input into the build system.
// Next-Id: 10
message SourceFile {
reserved 7;
// The name of the source file (a label).
required string name = 1;
// The location of the source file. This is a path with line numbers, not
// a label in the build system.
optional string location = 2;
// Labels of .bzl (Skylark) files that are transitively loaded in this BUILD
// file. This is present only when the SourceFile represents a BUILD file that
// loaded .bzl files.
// TODO(bazel-team): Rename this field.
repeated string subinclude = 3;
// Labels of package groups that are mentioned in the visibility declaration
// for this source file.
repeated string package_group = 4;
// Labels mentioned in the visibility declaration (including :__pkg__ and
// //visibility: ones)
repeated string visibility_label = 5;
// The package-level features enabled for this package. Only present if the
// SourceFile represents a BUILD file.
repeated string feature = 6;
// License attribute for the file.
optional License license = 8;
// True if the package contains an error. Only present if the SourceFile
// represents a BUILD file.
optional bool package_contains_errors = 9;
}
// A file that is the output of a build rule.
message GeneratedFile {
// The name of the generated file (a label).
required string name = 1;
// The label of the target that generates the file.
required string generating_rule = 2;
// The path of the output file (not a label).
optional string location = 3;
}
// A target from a blaze query execution. Similar to the Attribute message,
// the Discriminator is used to determine which field contains information.
// For any given type, only one of these can be populated in a single Target.
message Target {
enum Discriminator {
RULE = 1;
SOURCE_FILE = 2;
GENERATED_FILE = 3;
PACKAGE_GROUP = 4;
ENVIRONMENT_GROUP = 5;
}
// The type of target contained in the message.
required Discriminator type = 1;
// If this target represents a rule, the rule is stored here.
optional Rule rule = 2;
// A file that is not generated by the build system (version controlled
// or created by the test harness).
optional SourceFile source_file = 3;
// A generated file that is the output of a rule.
optional GeneratedFile generated_file = 4;
// A package group.
optional PackageGroup package_group = 5;
// An environment group.
optional EnvironmentGroup environment_group = 6;
}
// Container for all of the blaze query results.
message QueryResult {
// All of the targets returned by the blaze query.
repeated Target target = 1;
}
////////////////////////////////////////////////////////////////////////////
// Messages dealing with querying the BUILD language itself. For now, this is
// quite simplistic: Blaze can only tell the names of the rule classes, their
// attributes with their type.
// Information about allowed rule classes for a specific attribute of a rule.
message AllowedRuleClassInfo {
enum AllowedRuleClasses {
ANY = 1; // Any rule is allowed to be in this attribute
SPECIFIED = 2; // Only the explicitly listed rules are allowed
}
required AllowedRuleClasses policy = 1;
// Rule class names of rules allowed in this attribute, e.g "cc_library",
// "py_binary". Only present if the allowed_rule_classes field is set to
// SPECIFIED.
repeated string allowed_rule_class = 2;
}
// This message represents a single attribute of a single rule.
// See docs.bazel.build/versions/master/skylark/lib/attr.html.
message AttributeDefinition {
required string name = 1; // e.g. "name", "srcs"
required Attribute.Discriminator type = 2;
optional bool mandatory = 3;
optional AllowedRuleClassInfo allowed_rule_classes = 4; // type=label*
optional string documentation = 5;
optional bool allow_empty = 6; // type=*_list|*_dict
optional bool allow_single_file = 7; // type=label
optional AttributeValue default = 9; // simple (not computed/late-bound) values only
optional bool executable = 10; // type=label
optional bool configurable = 11;
optional bool nodep = 12; // label-valued edge does not establish a dependency
optional bool cfg_is_host = 13; // edge entails a transition to "host" configuration
}
// An AttributeValue represents the value of an attribute.
// A single field, determined by the attribute type, is populated.
//
// It is used only for AttributeDefinition.default. Attribute and
// SelectorEntry do their own thing for unfortunate historical reasons.
message AttributeValue {
optional int32 int = 1; // type=int|tristate
optional string string = 2; // type=string|label|output
optional bool bool = 3; // type=bool
repeated AttributeValue list = 4; // type=*_list|distrib
repeated DictEntry dict = 5; // type=*_dict
message DictEntry {
required string key = 1;
required AttributeValue value = 2;
}
}
message RuleDefinition {
required string name = 1;
// Only contains documented attributes
repeated AttributeDefinition attribute = 2;
optional string documentation = 3;
// Only for build extensions: label to file that defines the extension
optional string label = 4;
}
message BuildLanguage {
// Only contains documented rule definitions
repeated RuleDefinition rule = 1;
}
================================================
FILE: gitops/commitmsg/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library", "go_test")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["commitmsg.go"],
importpath = "github.com/adobe/rules_gitops/gitops/commitmsg",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["commitmsg_test.go"],
deps = [":go_default_library"],
)
================================================
FILE: gitops/commitmsg/commitmsg.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package commitmsg
import (
"log"
"strings"
)
const begin = "--- gitops targets begin ---"
const end = "--- gitops targets end ---"
// ExtractTargets extracts list of gitops targets used in a commit
func ExtractTargets(msg string) (packages []string) {
betweenMarkers := false
for _, s := range strings.Split(msg, "\n") {
switch s {
case begin:
betweenMarkers = true
case end:
betweenMarkers = false
default:
if betweenMarkers {
packages = append(packages, s)
}
}
}
if betweenMarkers {
log.Print("Unable to find end marker in commit message")
}
return
}
// Generate generates a commit message from a list of targets, including git branch and commit info above the targets block
func Generate(targets []string, branchName, gitCommit string) string {
var sb strings.Builder
sb.WriteByte('\n')
sb.WriteString("Branch: ")
sb.WriteString(branchName)
sb.WriteByte('\n')
sb.WriteString("Commit: ")
sb.WriteString(gitCommit)
sb.WriteByte('\n')
sb.WriteString(begin)
sb.WriteByte('\n')
for _, t := range targets {
sb.WriteString(t)
sb.WriteByte('\n')
}
sb.WriteString(end)
sb.WriteByte('\n')
return sb.String()
}
================================================
FILE: gitops/commitmsg/commitmsg_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package commitmsg_test
import (
"fmt"
"reflect"
"testing"
"github.com/adobe/rules_gitops/gitops/commitmsg"
)
func TestRoundtrip(t *testing.T) {
targets := []string{"target1", "target2"}
msg := commitmsg.Generate(targets, "my-branch", "abc123")
t2 := commitmsg.ExtractTargets(msg)
if !reflect.DeepEqual(targets, t2) {
t.Errorf("Unexpected targets after parsing: %v", t2)
}
}
func ExampleGenerate() {
targets := []string{"target1", "target2"}
msg := commitmsg.Generate(targets, "my-branch", "abc123")
fmt.Println(msg)
// Output:
// Branch: my-branch
// Commit: abc123
// --- gitops targets begin ---
// target1
// target2
// --- gitops targets end ---
}
================================================
FILE: gitops/defs.bzl
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""
GitOps rules public interface
"""
load("//gitops/private:gitops.bzl", _gitops = "gitops")
load("//gitops/private:k8s_deploy.bzl", _k8s_deploy = "k8s_deploy")
k8s_deploy = _k8s_deploy
gitops = _gitops
================================================
FILE: gitops/digester/BUILD.bazel
================================================
# Copyright 2024 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["digester.go"],
importpath = "github.com/adobe/rules_gitops/gitops/digester",
visibility = ["//visibility:public"],
)
================================================
FILE: gitops/digester/digester.go
================================================
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package digester
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"log"
"os"
)
// CalculateDigest calculates the SHA256 digest of a file specified by the given path
func CalculateDigest(path string) string {
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return ""
}
fi, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer fi.Close()
h := sha256.New()
if _, err := io.Copy(h, fi); err != nil {
log.Fatal(err)
}
return hex.EncodeToString(h.Sum(nil))
}
// GetDigest retrieves the digest of a file from a file with the same name but with a ".digest" extension
func GetDigest(path string) string {
digestPath := path + ".digest"
if _, err := os.Stat(digestPath); errors.Is(err, os.ErrNotExist) {
return ""
}
digest, err := os.ReadFile(digestPath)
if err != nil {
log.Fatal(err)
}
return string(digest)
}
// VerifyDigest verifies the integrity of a file by comparing its calculated digest with the stored digest
func VerifyDigest(path string) bool {
return CalculateDigest(path) == GetDigest(path)
}
// SaveDigest calculates the digest of a file at the given path and saves it to a file with the same name but with a ".digest" extension.
func SaveDigest(path string) {
digest := CalculateDigest(path)
digestPath := path + ".digest"
err := os.WriteFile(digestPath, []byte(digest), 0666)
if err != nil {
log.Fatal(err)
}
}
================================================
FILE: gitops/exec/BUILD
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["exec.go"],
importpath = "github.com/adobe/rules_gitops/gitops/exec",
visibility = ["//visibility:public"],
)
================================================
FILE: gitops/exec/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["exec.go"],
importpath = "github.com/adobe/rules_gitops/gitops/exec",
visibility = ["//visibility:public"],
)
================================================
FILE: gitops/exec/exec.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package exec
import (
"log"
"os/exec"
"strings"
)
// Ex is a shortcut for executing the command in specified dir
func Ex(dir, name string, arg ...string) (output string, err error) {
log.Println("executing:", name, strings.Join(arg, " "))
cmd := exec.Command(name, arg...)
if dir != "" {
cmd.Dir = dir
}
b, err := cmd.CombinedOutput()
log.Printf("%s", string(b))
return string(b), err
}
// Mustex executes the command name arg... in directory dir
// it will exit with fatal error if execution was not successful
func Mustex(dir, name string, arg ...string) {
_, err := Ex(dir, name, arg...)
if err != nil {
log.Fatalf("ERROR: %s", err)
}
}
================================================
FILE: gitops/git/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = [
"git.go",
"server.go",
],
importpath = "github.com/adobe/rules_gitops/gitops/git",
visibility = ["//visibility:public"],
deps = ["//gitops/exec:go_default_library"],
)
================================================
FILE: gitops/git/bitbucket/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library", "go_test")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["bitbucket.go"],
importpath = "github.com/adobe/rules_gitops/gitops/git/bitbucket",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["bitbucket_test.go"],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
)
================================================
FILE: gitops/git/bitbucket/bitbucket.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package bitbucket
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)
var (
apiEndpoint = flag.String("bitbucket_api_pr_endpoint", "https://bitbucket.tubemogul.info/rest/api/1.0/projects/TM/repos/repo/pull-requests", "bitbucket pull request api endpoint with project and repo")
bitbucketUser = flag.String("bitbucket_user", os.Getenv("BITBUCKET_USER"), "bitbucket api user")
bitbucketPassword = flag.String("bitbucket_password", os.Getenv("BITBUCKET_PASSWORD"), "bitbucket api user password")
)
type project struct {
Key string `json:"key,omitempty"`
}
type repository struct {
Slug string `json:"slug,omitempty"`
Project project `json:"project"`
}
type pullrequestEndpoint struct {
ID string `json:"id,omitempty"`
Repository repository `json:"repository,omitempty"`
}
type account struct {
User user `json:"user"`
}
type user struct {
Name string `json:"name,omitempty"`
}
type pullrequest struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
State string `json:"state,omitempty"`
Open bool `json:"open"`
Closed bool `json:"closed"`
FromRef *pullrequestEndpoint `json:"fromRef,omitempty"`
ToRef *pullrequestEndpoint `json:"toRef,omitempty"`
Locked bool `json:"locked"`
Reviewers []account `json:"reviewers,omitempty"`
}
// CreatePR creates a pull request using branch names from and to
func CreatePR(from, to, title, body string) error {
repo := repository{
Slug: "repo",
Project: project{"TM"},
}
prReq := pullrequest{
Title: title,
Description: body,
State: "OPEN",
Open: true,
Closed: false,
FromRef: &pullrequestEndpoint{
ID: "refs/heads/" + from,
Repository: repo,
},
ToRef: &pullrequestEndpoint{
ID: "refs/heads/" + to,
Repository: repo,
},
Locked: false,
Reviewers: []account{},
}
json, err := json.Marshal(&prReq)
if err != nil {
return fmt.Errorf("Unable to marshal CreatePR request: %w", err)
}
req, err := http.NewRequest("POST", *apiEndpoint, bytes.NewBuffer(json))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth(*bitbucketUser, *bitbucketPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Unable to send CreatePR request: %w", err)
}
log.Printf("bitbucket api response: %s", resp.Status)
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
log.Print("bitbucket response: ", string(responseBody))
// 201 created
// 409 already exists
if resp.StatusCode == 201 {
log.Print("PR was created")
return nil
}
if resp.StatusCode == 409 {
log.Print("reusing existing PR")
return nil
}
return fmt.Errorf("Unrecognized bitbucket response %d", resp.StatusCode)
}
================================================
FILE: gitops/git/bitbucket/bitbucket_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package bitbucket
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestCreatePRRemote(t *testing.T) {
t.Skip("Manual")
user := "********"
pass := "*************"
bitbucketUser = &user
bitbucketPassword = &pass
err := CreatePR("deploy/test1", "feature/AP-0000", "test", "hello world")
if err != nil {
t.Error("Unexpected error from server: ", err)
}
}
func TestCreatePRNew(t *testing.T) {
var buf []byte
var srverr error
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf, srverr = ioutil.ReadAll(r.Body)
http.Error(w, "Created", 201)
fmt.Fprintln(w, "PR created")
}))
defer ts.Close()
oldendpoint := *apiEndpoint
defer func() { *apiEndpoint = oldendpoint }()
*apiEndpoint = ts.URL
err := CreatePR("deploy/test1", "feature/AP-0000", "test", "hello world")
if err != nil {
t.Error("Unexpected error from server: ", err)
}
if srverr != nil {
t.Error("Unexpected error: ", srverr)
}
expectedreq := `{"title":"test","description":"hello world","state":"OPEN","open":true,"closed":false,"fromRef":{"id":"refs/heads/deploy/test1","repository":{"slug":"repo","project":{"key":"TM"}}},"toRef":{"id":"refs/heads/feature/AP-0000","repository":{"slug":"repo","project":{"key":"TM"}}},"locked":false}`
if string(buf) != expectedreq {
t.Error("Unexpected request body: ", string(buf))
}
}
================================================
FILE: gitops/git/bitbucket/testdata/create_pr.json
================================================
{
"title": "Deploy to test 1",
"description": "Deploy to test 1.",
"state": "OPEN",
"open": true,
"closed": false,
"fromRef": {
"id": "refs/heads/deploy/test_1",
"repository": {
"slug": "repo",
"name": null,
"project": {
"key": "TM"
}
}
},
"toRef": {
"id": "refs/heads/master",
"repository": {
"slug": "repo",
"name": null,
"project": {
"key": "TM"
}
}
},
"locked": false,
"reviewers": [
{
"user": {
"name": "aleksey.pesternikov"
}
}
]
}
================================================
FILE: gitops/git/git.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package git
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"os"
oe "os/exec"
"path/filepath"
"strings"
"github.com/adobe/rules_gitops/gitops/exec"
)
var (
git = "git"
)
// Clone clones a repository. Pass the full repository name, such as
// "https://aleksey.pesternikov@bitbucket.tubemogul.info/scm/tm/repo.git" as the repo.
// Cloned directory will be clean of local changes with primaryBranch branch checked out.
// repo: https://aleksey.pesternikov@bitbucket.tubemogul.info/scm/tm/repo.git
// dir: /tmp/cloudrepo
// mirrorDir: optional (if not empty) local mirror of the repository
func Clone(repo, dir, mirrorDir, primaryBranch, gitopsPath string) (*Repo, error) {
if err := os.RemoveAll(dir); err != nil {
return nil, fmt.Errorf("Unable to clone repo: %w", err)
}
remoteName := "origin"
args := []string{"clone", "--no-checkout", "--single-branch", "--branch", primaryBranch, "--filter=blob:none", "--no-tags", "--origin", remoteName}
if mirrorDir != "" {
args = append(args, "--reference", mirrorDir)
}
args = append(args, repo, dir)
exec.Mustex("", "git", args...)
// Enable sparse-checkout when restricting to a subdir
if !isRootPath(gitopsPath) {
exec.Mustex(dir, "git", "config", "--local", "core.sparsecheckout", "true")
genPath := fmt.Sprintf("%s/\n", gitopsPath)
if err := ioutil.WriteFile(filepath.Join(dir, ".git/info/sparse-checkout"), []byte(genPath), 0644); err != nil {
return nil, fmt.Errorf("Unable to create .git/info/sparse-checkout: %w", err)
}
}
exec.Mustex(dir, "git", "checkout", primaryBranch)
return &Repo{
Dir: dir,
RemoteName: remoteName,
}, nil
}
// Repo is a clone of a git repository. Create with Clone, and don't
// forget to clean it up after.
type Repo struct {
// Dir is the location of the git repo.
Dir string
// RemoteName is the name of the remote that tracks upstream repository.
RemoteName string
}
// Clean cleans up the repo
func (r *Repo) Clean() error {
return os.RemoveAll(r.Dir)
}
// Fetch branches from the remote repository based on a specified pattern.
// The branches will be be added to the list tracked remote branches ready to be pushed.
func (r *Repo) Fetch(pattern string) {
exec.Mustex(r.Dir, "git", "remote", "set-branches", "--add", r.RemoteName, pattern)
exec.Mustex(r.Dir, "git", "fetch", "--force", "--filter=blob:none", "--no-tags", r.RemoteName)
}
// SwitchToBranch switch the repo to specified branch and checkout primaryBranch files over it.
// if branch does not exist it will be created
func (r *Repo) SwitchToBranch(branch, primaryBranch string) (new bool) {
if _, err := exec.Ex(r.Dir, "git", "checkout", branch); err != nil {
// error checking out, create new
exec.Mustex(r.Dir, "git", "branch", branch, primaryBranch)
exec.Mustex(r.Dir, "git", "checkout", branch)
return true
}
return false
}
// RecreateBranch discards a branch content and reset it from primaryBranch.
func (r *Repo) RecreateBranch(branch, primaryBranch string) {
exec.Mustex(r.Dir, "git", "checkout", primaryBranch)
exec.Mustex(r.Dir, "git", "branch", "-f", branch, primaryBranch)
exec.Mustex(r.Dir, "git", "checkout", branch)
}
// GetLastCommitMessage fetches the commit message from the most recent change of the branch
func (r *Repo) GetLastCommitMessage() (msg string) {
msg, err := exec.Ex(r.Dir, "git", "log", "-1", "--pretty=%B")
if err != nil {
return ""
}
return msg
}
// Commit all changes to the current branch. returns true if there were any changes
func (r *Repo) Commit(message, gitopsPath string) bool {
if isRootPath(gitopsPath) {
exec.Mustex(r.Dir, "git", "add", ".")
} else {
exec.Mustex(r.Dir, "git", "add", gitopsPath)
}
if r.IsClean() {
return false
}
exec.Mustex(r.Dir, "git", "commit", "-a", "-m", message)
return true
}
// RestoreFile restores the specified file in the repository to its original state
func (r *Repo) RestoreFile(fileName string) {
exec.Mustex(r.Dir, "git", "checkout", "--", fileName)
}
// GetChangedFiles returns a list of files that have been changed in the repository
func (r *Repo) GetChangedFiles() []string {
s, err := exec.Ex(r.Dir, "git", "diff", "--name-only")
if err != nil {
log.Fatalf("ERROR: %s", err)
}
var files []string
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
files = append(files, sc.Text())
}
if err := sc.Err(); err != nil {
log.Fatalf("ERROR: %s", err)
}
return files
}
// IsClean returns true if there is no local changes (nothing to commit)
func (r *Repo) IsClean() bool {
cmd := oe.Command("git", "status", "--porcelain")
cmd.Dir = r.Dir
b, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("ERROR: %s", err)
}
return len(b) == 0
}
// Push pushes all local changes to the remote repository
// all changes should be already commited
func (r *Repo) Push(branches []string) {
args := append([]string{"push", r.RemoteName, "-f", "--set-upstream"}, branches...)
exec.Mustex(r.Dir, "git", args...)
}
// isRootPath is an internal helper to detect "full repo" case.
func isRootPath(gitopsPath string) bool {
return gitopsPath == "" || gitopsPath == "."
}
================================================
FILE: gitops/git/github/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["github.go"],
importpath = "github.com/adobe/rules_gitops/gitops/git/github",
visibility = ["//visibility:public"],
deps = [
"@com_github_google_go_github_v32//github:go_default_library",
"@org_golang_x_oauth2//:go_default_library",
],
)
================================================
FILE: gitops/git/github/github.go
================================================
package github
import (
"context"
"errors"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/google/go-github/v32/github"
"golang.org/x/oauth2"
)
var (
repoOwner = flag.String("github_repo_owner", "", "the owner user/organization to use for github api requests")
repo = flag.String("github_repo", "", "the repo to use for github api requests")
pat = flag.String("github_access_token", os.Getenv("GITHUB_TOKEN"), "the access token to authenticate requests")
githubEnterpriseHost = flag.String("github_enterprise_host", "", "The host name of the private enterprise github, e.g. git.corp.adobe.com")
)
func CreatePR(from, to, title, body string) error {
if *repoOwner == "" {
return errors.New("github_repo_owner must be set")
}
if *repo == "" {
return errors.New("github_repo must be set")
}
if *pat == "" {
return errors.New("github_access_token must be set")
}
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: *pat},
)
tc := oauth2.NewClient(ctx, ts)
var gh *github.Client
if *githubEnterpriseHost != "" {
baseUrl := "https://" + *githubEnterpriseHost + "/api/v3/"
uploadUrl := "https://" + *githubEnterpriseHost + "/api/uploads/"
var err error
gh, err = github.NewEnterpriseClient(baseUrl, uploadUrl, tc)
if err != nil {
log.Println("Error in creating github client", err)
return nil
}
} else {
gh = github.NewClient(tc)
}
pr := &github.NewPullRequest{
Title: &title,
Head: &from,
Base: &to,
Body: &body,
Issue: nil,
MaintainerCanModify: new(bool),
Draft: new(bool),
}
createdPr, resp, err := gh.PullRequests.Create(ctx, *repoOwner, *repo, pr)
if err == nil {
log.Println("Created PR: ", *createdPr.URL)
return err
}
if resp.StatusCode == http.StatusUnprocessableEntity {
// Handle the case: "Create PR" request fails because it already exists
log.Println("Reusing existing PR")
return nil
}
// All other github responses
defer resp.Body.Close()
responseBody, readingErr := ioutil.ReadAll(resp.Body)
if readingErr != nil {
log.Println("cannot read response body")
} else {
log.Println("github response: ", string(responseBody))
}
return err
}
================================================
FILE: gitops/git/gitlab/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["gitlab.go"],
importpath = "github.com/adobe/rules_gitops/gitops/git/gitlab",
visibility = ["//visibility:public"],
deps = ["@com_github_xanzy_go_gitlab//:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["gitlab_test.go"],
embed = [":go_default_library"],
)
================================================
FILE: gitops/git/gitlab/gitlab.go
================================================
package gitlab
import (
"errors"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/xanzy/go-gitlab"
)
var (
gitlabHost = flag.String("gitlab_host", "https://gitlab.com", "The host name of the gitlab instance")
repo = flag.String("gitlab_repo", "", "the repo to use for gitlab api requests")
accessToken = flag.String("gitlab_access_token", os.Getenv("GITLAB_TOKEN"), "the access token to authenticate requests")
)
func CreatePR(from, to, title, body string) error {
if *accessToken == "" {
return errors.New("gitlab_access_token must be set")
}
opts := gitlab.CreateMergeRequestOptions{
Title: &title,
Description: nil,
SourceBranch: &from,
TargetBranch: &to,
Labels: nil,
AssigneeID: nil,
AssigneeIDs: nil,
ReviewerIDs: nil,
TargetProjectID: nil,
MilestoneID: nil,
RemoveSourceBranch: nil,
Squash: nil,
AllowCollaboration: nil,
}
gl, err := gitlab.NewClient(*accessToken, gitlab.WithBaseURL(*gitlabHost))
if err != nil {
return err
}
createdPr, resp, err := gl.MergeRequests.CreateMergeRequest(*repo, &opts)
if err == nil {
log.Println("Created MR: ", createdPr.WebURL)
return nil
}
if resp.StatusCode == http.StatusConflict {
// Handle the case: "Create MR" request fails because it already exists for this source branch
log.Println("Reusing existing MR")
return nil
}
// All other gitlab responses
defer resp.Body.Close()
responseBody, readingErr := ioutil.ReadAll(resp.Body)
if readingErr != nil {
log.Println("cannot read response body")
} else {
log.Println("gitlab response: ", string(responseBody))
}
return err
}
================================================
FILE: gitops/git/gitlab/gitlab_test.go
================================================
package gitlab
import "testing"
func TestCreatePRRemote(t *testing.T) {
t.Skip("Manual")
var (
testGitlabToken = "********"
)
accessToken = &testGitlabToken
type args struct {
from string
to string
title string
body string
}
tests := []struct {
repo string
args args
wantErr bool
}{
{
repo: "cotocisternas/rules_gitops_gitlab_test",
args: args{
from: "feature/gitlab-test",
to: "master",
title: "test_gitlab",
},
wantErr: false,
},
{
repo: "cotocisternas/rules_gitops_gitlab_test",
args: args{
from: "feature/gitlab-test",
to: "master",
title: "test_gitlab",
body: "hello world",
},
wantErr: false,
},
{
repo: "petabytecl/subgroup_rules_gitops_gitlab_test/rules_gitops_gitlab_test",
args: args{
from: "feature/gitlab-test",
to: "master",
title: "test_gitlab",
body: "hello world",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.repo, func(t *testing.T) {
repo = &tt.repo
if err := CreatePR(tt.args.from, tt.args.to, tt.args.title, tt.args.body); (err != nil) != tt.wantErr {
t.Errorf("CreatePR() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
================================================
FILE: gitops/git/server.go
================================================
package git
type Server interface {
CreatePR(from, to, title, body string) error
}
type ServerFunc func(from, to, title, body string) error
func (f ServerFunc) CreatePR(from, to, title, body string) error {
if body == "" {
body = title
}
return f(from, to, title, body)
}
================================================
FILE: gitops/prer/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_binary", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["create_gitops_prs.go"],
importpath = "github.com/adobe/rules_gitops/gitops/prer",
visibility = ["//visibility:private"],
deps = [
"//gitops/analysis:go_default_library",
"//gitops/bazel:go_default_library",
"//gitops/commitmsg:go_default_library",
"//gitops/digester:go_default_library",
"//gitops/exec:go_default_library",
"//gitops/git:go_default_library",
"//gitops/git/bitbucket:go_default_library",
"//gitops/git/github:go_default_library",
"//gitops/git/gitlab:go_default_library",
"//templating/fasttemplate:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
],
)
go_binary(
name = "create_gitops_prs",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
================================================
FILE: gitops/prer/create_gitops_prs.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
oe "os/exec"
"strings"
"github.com/adobe/rules_gitops/gitops/analysis"
"github.com/adobe/rules_gitops/gitops/bazel"
"github.com/adobe/rules_gitops/gitops/commitmsg"
"github.com/adobe/rules_gitops/gitops/digester"
"github.com/adobe/rules_gitops/gitops/exec"
"github.com/adobe/rules_gitops/gitops/git"
"github.com/adobe/rules_gitops/gitops/git/bitbucket"
"github.com/adobe/rules_gitops/gitops/git/github"
"github.com/adobe/rules_gitops/gitops/git/gitlab"
"github.com/adobe/rules_gitops/templating/fasttemplate"
"google.golang.org/protobuf/proto"
)
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
var (
releaseBranch = flag.String("release_branch", "main", "filter gitops targets by release branch")
bazelCmd = flag.String("bazel_cmd", "tools/bazel", "bazel binary to use")
workspace = flag.String("workspace", "", "path to workspace root")
repo = flag.String("git_repo", "", "git repo location")
gitMirror = flag.String("git_mirror", "", "git mirror location, like /mnt/mirror/bitbucket.tubemogul.info/tm/repo.git for jenkins")
gitopsPath = flag.String("gitops_path", "cloud", "location to store files in repo.")
gitopsTmpDir = flag.String("gitops_tmpdir", os.TempDir(), "location to check out git tree with /cloud.")
target = flag.String("target", "//... except //experimental/...", "target to scan. Useful for debugging only")
prInto = flag.String("gitops_pr_into", "main", "use this branch as the source branch and target for deployment PR")
prBody = flag.String("gitops_pr_body", "", "a body message for deployment PR")
prTitle = flag.String("gitops_pr_title", "", "a title for deployment PR")
branchName = flag.String("branch_name", "unknown", "Branch name to use in commit message")
gitCommit = flag.String("git_commit", "unknown", "Git commit to use in commit message")
deploymentBranchPrefix = flag.String("deployment_branch_prefix", "deploy/", "the prefix to add to all deployment branch names")
deploymentBranchSuffix = flag.String("deployment_branch_suffix", "", "suffix to add to all deployment branch names")
gitHost = flag.String("git_server", "bitbucket", "the git server api to use. 'bitbucket', 'github' or 'gitlab'")
stamp = flag.Bool("stamp", false, "Stamp results of gitops targets with volatile information")
dryRun = flag.Bool("dry_run", false, "Do not create PRs, just print what would be done")
)
func bazelQuery(query string) *analysis.CqueryResult {
log.Println("Executing bazel cquery ", query)
cmd := oe.Command(*bazelCmd, "cquery", query, "--output=proto")
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
go func() {
io.Copy(os.Stderr, stderr)
}()
buildproto, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
qr := &analysis.CqueryResult{}
if err := proto.Unmarshal(buildproto, qr); err != nil {
log.Fatal(err)
}
return qr
}
func getGitStatusDict(workdir *git.Repo, gitCommit, branchName string) map[string]interface{} {
utcDate, err := exec.Ex("", "date", "-u")
if err != nil {
log.Fatal(err)
}
utcDate = strings.TrimSpace(utcDate)
ctx := map[string]interface{}{
"GIT_REVISION": gitCommit,
"UTC_DATE": utcDate,
"GIT_BRANCH": branchName,
}
return ctx
}
func stampFile(fullPath string, ctx map[string]interface{}) {
template, err := os.ReadFile(fullPath)
if err != nil {
log.Fatal(err)
}
outf, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Fatal(err)
}
defer outf.Close()
_, err = fasttemplate.Execute(string(template), "{{", "}}", outf, ctx)
if err != nil {
log.Fatal(err)
}
}
func main() {
flag.Parse()
if *workspace != "" {
if err := os.Chdir(*workspace); err != nil {
log.Fatal(err)
}
}
var gitServer git.Server
switch *gitHost {
case "github":
gitServer = git.ServerFunc(github.CreatePR)
case "gitlab":
gitServer = git.ServerFunc(gitlab.CreatePR)
case "bitbucket":
gitServer = git.ServerFunc(bitbucket.CreatePR)
default:
log.Fatalf("unknown vcs host: %s", *gitHost)
}
q := fmt.Sprintf("attr(deployment_branch, \".+\", attr(release_branch_prefix, \"%s\", kind(gitops, %s)))", *releaseBranch, *target)
qr := bazelQuery(q)
releaseTrains := make(map[string][]string)
for _, t := range qr.Results {
var releaseTrain string
for _, a := range t.Target.GetRule().GetAttribute() {
if a.GetName() == "deployment_branch" {
releaseTrain = a.GetStringValue()
}
}
releaseTrains[releaseTrain] = append(releaseTrains[releaseTrain], t.Target.Rule.GetName())
}
if (len(releaseTrains)) == 0 {
log.Println("No matching targets found")
return
}
for train, targets := range releaseTrains {
fmt.Println(train)
for _, t := range targets {
fmt.Println(" ", t)
}
}
gitopsdir, err := os.MkdirTemp(*gitopsTmpDir, "gitops")
if err != nil {
log.Fatalf("Unable to create tempdir in %s: %v", *gitopsTmpDir, err)
}
defer os.RemoveAll(gitopsdir)
workdir, err := git.Clone(*repo, gitopsdir, *gitMirror, *prInto, *gitopsPath)
if err != nil {
log.Fatalf("Unable to clone repo: %v", err)
}
workdir.Fetch(*deploymentBranchPrefix + "*")
var updatedGitopsBranches []string
for train, targets := range releaseTrains {
log.Println("train", train)
branch := *deploymentBranchPrefix + train + *deploymentBranchSuffix
newBranch := workdir.SwitchToBranch(branch, *prInto)
if !newBranch {
// Find if we need to recreate the branch because target was deleted
msg := workdir.GetLastCommitMessage()
targetset := make(map[string]bool)
for _, t := range targets {
targetset[t] = true
}
oldtargets := commitmsg.ExtractTargets(msg)
for _, t := range oldtargets {
if !targetset[t] {
// target t is not present in a new list
workdir.RecreateBranch(branch, *prInto)
break
}
}
}
for _, target := range targets {
log.Println("train", train, "target", target)
bin := bazel.TargetToExecutable(target)
exec.Mustex("", bin, "--deployment_root", gitopsdir)
}
if *stamp {
changedFiles := workdir.GetChangedFiles()
if len(changedFiles) > 0 {
ctx := getGitStatusDict(workdir, *gitCommit, *branchName)
for _, filePath := range changedFiles {
fullPath := gitopsdir + "/" + filePath
if digester.VerifyDigest(fullPath) {
workdir.RestoreFile(fullPath)
} else {
digester.SaveDigest(fullPath)
stampFile(fullPath, ctx)
}
}
}
}
if workdir.Commit(fmt.Sprintf("GitOps for release branch %s from %s commit %s\n%s", *releaseBranch, *branchName, *gitCommit, commitmsg.Generate(targets, *branchName, *gitCommit)), *gitopsPath) {
log.Println("branch", branch, "has changes, push is required")
updatedGitopsBranches = append(updatedGitopsBranches, branch)
}
}
if len(updatedGitopsBranches) == 0 {
log.Println("No gitops changes to push")
return
}
if *dryRun {
log.Println("dry-run: updated gitops branches: ", updatedGitopsBranches)
log.Println("dry-run: skipping push")
} else {
workdir.Push(updatedGitopsBranches)
}
for _, branch := range updatedGitopsBranches {
if *dryRun {
log.Println("dry-run: skipping PR creation: branch ", branch, "into ", *prInto)
continue
}
title := *prTitle
if title == "" {
title = fmt.Sprintf("GitOps deployment %s", branch)
}
body := *prBody
if body == "" {
body = branch
}
if err := gitServer.CreatePR(branch, *prInto, title, body); err != nil {
log.Fatal("unable to create PR: ", err)
}
}
}
================================================
FILE: gitops/private/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
package(
default_visibility = ["//gitops:__subpackages__"],
)
exports_files([
"nameprefix_deployment_labels_config.yaml",
"namesuffix_deployment_labels_config.yaml",
"k8s_gitops.sh.tpl",
])
bzl_library(
name = "k8s_deploy",
srcs = ["k8s_deploy.bzl"],
deps = [
":gitops",
"//kubectl:defs",
"//kustomize:defs",
],
)
bzl_library(
name = "gitops",
srcs = ["gitops.bzl"],
deps = [
"//adapters:providers",
"//kustomize:defs",
],
)
================================================
FILE: gitops/private/gitops.bzl
================================================
"""GitOps rule for generating deployment manifests."""
load("//adapters:providers.bzl", "K8sPushInfo")
load("//kustomize:defs.bzl", "KustomizeInfo")
# Convert short_path to runfiles manifest path for use with rlocation
def _to_manifest_path(ctx, file):
if file.short_path.startswith("../"):
return "external/" + file.short_path[3:]
else:
return ctx.workspace_name + "/" + file.short_path
def _image_push_statements(
ctx,
kustomize_objs,
files = []):
statements = ""
trans_img_pushes = depset(transitive = [obj[KustomizeInfo].image_pushes for obj in kustomize_objs]).to_list()
statements += "\n".join([
"echo pushing {}/{}".format(exe[K8sPushInfo].registry, exe[K8sPushInfo].repository)
for exe in trans_img_pushes
if hasattr(exe[K8sPushInfo], "pusher")
]) + "\n"
statements += "\n".join([
" async \"$(rlocation %s)\"" % _to_manifest_path(ctx, exe[K8sPushInfo].pusher.files_to_run.executable)
for exe in trans_img_pushes
if hasattr(exe[K8sPushInfo], "pusher")
]) + "\n waitpids\n"
# files += [obj.files_to_run.executable for obj in trans_img_pushes]
dep_runfiles = [obj[K8sPushInfo].pusher.default_runfiles for obj in trans_img_pushes if hasattr(obj[K8sPushInfo], "pusher")]
return statements, files, dep_runfiles
def _remove_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s
def _remove_prefixes(s, prefixes):
for prefix in prefixes:
s = _remove_prefix(s, prefix)
return s
def _gitops_impl(ctx):
cluster = ctx.attr.cluster
strip_prefixes = ctx.attr.strip_prefixes
files = []
push_statements, files, pushes_runfiles = _image_push_statements(ctx, ctx.attr.srcs, files)
statements = ""
namespace = ctx.attr.namespace
for inattr in ctx.attr.srcs:
if "{" in namespace:
fail("unable to gitops namespace with placeholders %s" % inattr.label) #mynamespace should not be gitopsed
for infile in inattr.files.to_list():
statements += ("echo $TARGET_DIR/{gitops_path}/{namespace}/{cluster}/{file}\n" +
"mkdir -p $TARGET_DIR/{gitops_path}/{namespace}/{cluster}\n" +
"echo '# GENERATED BY {rulename} -> {gitopsrulename}' > $TARGET_DIR/{gitops_path}/{namespace}/{cluster}/{file}\n" +
"{template_engine} --template={infile} --variable=NAMESPACE={namespace} --stamp_info_file={info_file} >> $TARGET_DIR/{gitops_path}/{namespace}/{cluster}/{file}\n").format(
infile = infile.path,
rulename = inattr.label,
gitopsrulename = ctx.label,
namespace = namespace,
gitops_path = ctx.attr.gitops_path,
cluster = cluster,
file = _remove_prefixes(infile.path.split("/")[-1], strip_prefixes),
template_engine = ctx.executable._template_engine.path,
info_file = ctx.file._info_file.path,
)
ctx.actions.expand_template(
template = ctx.file._template,
substitutions = {
"%{deployment_branch}": ctx.attr.deployment_branch,
"%{push_statements}": push_statements,
"%{statements}": statements,
},
output = ctx.outputs.executable,
)
runfiles = files + ctx.files.srcs + [ctx.executable._template_engine, ctx.file._info_file]
transitive = depset(transitive = [obj.default_runfiles.files for obj in ctx.attr.srcs])
rf = ctx.runfiles(files = runfiles, transitive_files = transitive).merge(
ctx.attr._bash_runfiles[DefaultInfo].default_runfiles,
)
for dep_rf in pushes_runfiles:
rf = rf.merge(dep_rf)
return [
DefaultInfo(runfiles = rf),
KustomizeInfo(
image_pushes = depset(transitive = [obj[KustomizeInfo].image_pushes for obj in ctx.attr.srcs]),
),
]
gitops = rule(
attrs = {
"srcs": attr.label_list(providers = (KustomizeInfo,)),
"cluster": attr.string(mandatory = True),
"namespace": attr.string(mandatory = True),
"deployment_branch": attr.string(),
"gitops_path": attr.string(),
"release_branch_prefix": attr.string(),
"strip_prefixes": attr.string_list(),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
"_template_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
"_template": attr.label(
default = Label("//gitops/private:k8s_gitops.sh.tpl"),
allow_single_file = True,
),
"_bash_runfiles": attr.label(
default = Label("@bazel_tools//tools/bash/runfiles"),
),
},
executable = True,
implementation = _gitops_impl,
)
================================================
FILE: gitops/private/k8s_deploy.bzl
================================================
"""Macro for creating Kubernetes deployment targets with GitOps support."""
load("//gitops/private:gitops.bzl", _gitops = "gitops")
load("//kubectl:defs.bzl", "kubectl_binary")
load("//kustomize:defs.bzl", "kustomization", "show")
def k8s_deploy(
name, # name of the rule is important for gitops, since it will become a part of the target manifest file name in /cloud
cluster = "dev",
user = "{BUILD_USER}",
namespace = None,
configmaps_srcs = None,
secrets_srcs = None,
configmaps_renaming = None, # configmaps renaming policy. Could be None or 'hash'.
manifests = None,
name_prefix = None,
name_suffix = None,
prefix_suffix_app_labels = False, # apply kustomize configuration to modify "app" labels in Deployments when name prefix or suffix applied
patches = None,
image_name_patches = {},
image_tag_patches = {},
substitutions = {}, # dict of template parameter substitutions. CLUSTER and NAMESPACE parameters are added automatically.
configurations = [], # additional kustomize configuration files. rules_gitops provides
common_labels = {}, # list of common labels to apply to all objects see commonLabels kustomize docs
common_annotations = {}, # list of common annotations to apply to all objects see commonAnnotations kustomize docs
deps = [],
deps_aliases = {},
images = [],
objects = [],
gitops = True, # make sure to use gitops = False to work with individual namespace. This option will be turned False if namespace is '{BUILD_USER}'
gitops_path = "cloud",
deployment_branch = None,
release_branch_prefix = "main",
start_tag = "{{",
end_tag = "}}",
tags = [],
visibility = None,
verify_images = True):
"""Generates Kubernetes deployment targets with optional GitOps support.
This macro creates kustomization targets and kubectl binaries for deploying
Kubernetes manifests. When gitops is enabled, it also creates a gitops target
for managing deployments through a GitOps workflow.
Args:
name: Name of the rule. Important for gitops since it becomes part of
the target manifest file name in the gitops_path directory.
cluster: Target Kubernetes cluster name. Defaults to "dev".
user: Kubernetes user for authentication. Defaults to "{BUILD_USER}"
which is substituted at runtime.
namespace: Target Kubernetes namespace. Required when gitops=True.
Defaults to "{BUILD_USER}" when gitops=False.
configmaps_srcs: List of source files for generating ConfigMaps.
secrets_srcs: List of source files for generating Secrets.
configmaps_renaming: ConfigMap renaming policy. Can be None (no renaming)
or "hash" (append content hash to names).
manifests: List of Kubernetes manifest files. Defaults to all .yaml and
.yaml.tpl files in the package via glob.
name_prefix: Prefix to add to all resource names via kustomize.
name_suffix: Suffix to add to all resource names via kustomize.
prefix_suffix_app_labels: If True, applies kustomize configuration to
modify "app" labels in Deployments when name_prefix or name_suffix
is applied.
patches: List of kustomize patches to apply to the manifests.
image_name_patches: Dict mapping original image names to new image names.
image_tag_patches: Dict mapping image names to new tags.
substitutions: Dict of template parameter substitutions. CLUSTER and
NAMESPACE parameters are added automatically and should not be
included.
configurations: List of additional kustomize configuration files.
common_labels: Dict of labels to apply to all Kubernetes objects.
See kustomize commonLabels documentation.
common_annotations: Dict of annotations to apply to all Kubernetes
objects. See kustomize commonAnnotations documentation.
deps: List of dependency targets.
deps_aliases: Dict mapping aliases to dependency targets.
images: List of container image targets to include in the deployment.
objects: List of additional Kubernetes objects to include.
gitops: If True, creates a gitops target for GitOps workflow. Set to
False to work with individual namespaces. Automatically set to
False if namespace is "{BUILD_USER}".
gitops_path: Directory path for gitops manifests. Defaults to "cloud".
deployment_branch: Git branch for deployments. If None, uses default.
release_branch_prefix: Prefix for release branches. Defaults to "main".
start_tag: Opening delimiter for template substitutions. Defaults to "{{".
end_tag: Closing delimiter for template substitutions. Defaults to "}}".
tags: List of tags to apply to all generated targets.
visibility: Visibility specification for generated targets.
verify_images: Whether or not to fail if a Bazel image could not be resolved.
"""
if not manifests:
manifests = native.glob(["*.yaml", "*.yaml.tpl"])
if prefix_suffix_app_labels:
configurations = configurations + [
str(Label("//gitops:nameprefix_deployment_labels_config.yaml")),
str(Label("//gitops:namesuffix_deployment_labels_config.yaml")),
]
for reservedname in ["CLUSTER", "NAMESPACE"]:
if substitutions.get(reservedname):
fail("do not put %s in substitutions parameter of k8s_deploy. It will be added autimatically" % reservedname)
substitutions = dict(substitutions)
substitutions["CLUSTER"] = cluster
# NAMESPACE substitution is deferred until test_setup/kubectl/gitops
if namespace == "{BUILD_USER}":
gitops = False
if not gitops:
# Mynamespace option
if not namespace:
namespace = "{BUILD_USER}"
kustomization(
name = name,
namespace = namespace,
configmaps_srcs = configmaps_srcs,
secrets_srcs = secrets_srcs,
# disable_name_suffix_hash is renamed to configmaps_renaming in recent Kustomize
disable_name_suffix_hash = (configmaps_renaming != "hash"),
images = images,
manifests = manifests,
substitutions = substitutions,
deps = deps,
deps_aliases = deps_aliases,
start_tag = start_tag,
end_tag = end_tag,
name_prefix = name_prefix,
name_suffix = name_suffix,
configurations = configurations,
common_labels = common_labels,
common_annotations = common_annotations,
patches = patches,
objects = objects,
image_name_patches = image_name_patches,
image_tag_patches = image_tag_patches,
tags = tags,
visibility = visibility,
verify_images = verify_images,
)
kubectl_binary(
name = name + ".apply",
srcs = [name],
cluster = cluster,
user = user,
namespace = namespace,
tags = tags,
visibility = visibility,
)
kubectl_binary(
name = name + ".delete",
srcs = [name],
command = "delete",
cluster = cluster,
push = False,
user = user,
namespace = namespace,
tags = tags,
visibility = visibility,
)
show(
name = name + ".show",
namespace = namespace,
src = name,
tags = tags,
visibility = visibility,
)
else:
# gitops
if not namespace:
fail("namespace must be defined for gitops k8s_deploy")
kustomization(
name = name,
namespace = namespace,
configmaps_srcs = configmaps_srcs,
secrets_srcs = secrets_srcs,
# disable_name_suffix_hash is renamed to configmaps_renaming in recent Kustomize
disable_name_suffix_hash = (configmaps_renaming != "hash"),
images = images,
manifests = manifests,
visibility = visibility,
substitutions = substitutions,
deps = deps,
deps_aliases = deps_aliases,
start_tag = start_tag,
end_tag = end_tag,
name_prefix = name_prefix,
name_suffix = name_suffix,
configurations = configurations,
common_labels = common_labels,
common_annotations = common_annotations,
patches = patches,
image_name_patches = image_name_patches,
image_tag_patches = image_tag_patches,
tags = tags,
)
kubectl_binary(
name = name + ".apply",
srcs = [name],
cluster = cluster,
user = user,
namespace = namespace,
tags = tags,
visibility = visibility,
)
_gitops(
name = name + ".gitops",
srcs = [name],
cluster = cluster,
namespace = namespace,
gitops_path = gitops_path,
strip_prefixes = [
namespace + "-",
cluster + "-",
],
deployment_branch = deployment_branch,
release_branch_prefix = release_branch_prefix,
tags = tags,
visibility = ["//visibility:public"],
)
show(
name = name + ".show",
src = name,
namespace = namespace,
tags = tags,
visibility = visibility,
)
================================================
FILE: gitops/private/k8s_gitops.sh.tpl
================================================
#!/usr/bin/env bash
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
set -o nounset
set -o pipefail
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
runfiles_export_envvars
DEPLOYMENT_ROOT=""
PERFORM_PUSH="1"
# parse command line parameters
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-r|--deployment_root|--deployment-root)
DEPLOYMENT_ROOT="$2"
shift # past argument
shift # past value
;;
--nopush)
PERFORM_PUSH=""
shift
;;
*) # unknown option
echo Unsupported parameter $1
exit 1
;;
esac
done
PIDS=()
function async() {
# Launch the command asynchronously and track its process id.
"$@" &
PIDS+=($!)
}
function waitpids() {
# Wait for all of the subprocesses, returning the exit code of the first failed process.
if [ "${#PIDS[@]}" != 0 ]; then
for pid in ${PIDS[@]}; do
wait ${pid} || return $?
done
fi
}
if [ "$PERFORM_PUSH" == "1" ]; then
%{push_statements}
fi
cd $BUILD_WORKSPACE_DIRECTORY
if [ "%{deployment_branch}" != "" -a "${DEPLOYMENT_ROOT}" != "" ] ; then
TARGET_DIR=${DEPLOYMENT_ROOT}
else
echo "--deployment-root or deployment_branch is not specified, using repo root"
TARGET_DIR=$BUILD_WORKSPACE_DIRECTORY
fi
# make sure that the script exits immediately if any command below fails
set -o errexit
%{statements}
================================================
FILE: gitops/private/nameprefix_deployment_labels_config.yaml
================================================
namePrefix:
- path: metadata/name
- path: spec/selector/matchLabels/app
kind: Deployment
- path: spec/template/metadata/labels/app
kind: Deployment
- path: spec/selector/matchLabels/app.kubernetes.io\/name
kind: Deployment
- path: spec/template/metadata/labels/app.kubernetes.io\/name
kind: Deployment
- path: spec/selector/app
kind: Service
- path: spec/selector/app.kubernetes.io\/name
kind: Service
================================================
FILE: gitops/private/namesuffix_deployment_labels_config.yaml
================================================
nameSuffix:
- path: metadata/name
- path: spec/selector/matchLabels/app
kind: Deployment
- path: spec/template/metadata/labels/app
kind: Deployment
- path: spec/selector/matchLabels/app.kubernetes.io\/name
kind: Deployment
- path: spec/template/metadata/labels/app.kubernetes.io\/name
kind: Deployment
- path: spec/selector/app
kind: Service
- path: spec/selector/app.kubernetes.io\/name
kind: Service
================================================
FILE: gitops/private/test/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("//adapters:external_image.bzl", "external_image")
load("//gitops:defs.bzl", "k8s_deploy")
load("//tools:util.bzl", "golden_test")
licenses(["notice"]) # Apache 2.0
k8s_deploy(
name = "deploy_test",
cluster = "testcluster",
deployment_branch = "test1",
gitops = True,
images = ["//gitops/private/test/images:k8s_image"],
manifests = [
":deployment.yaml",
],
namespace = "ci",
release_branch_prefix = "gitops_test_release_branch",
visibility = ["//visibility:public"],
)
golden_test(
name = "deploy_test_golden",
in_file = "deploy_test",
)
external_image(
name = "external_image",
digest = "sha:1234567890",
image = "gcr.io/repo/someimage:thetag",
)
k8s_deploy(
name = "external_image_test",
cluster = "testcluster",
deployment_branch = "test1",
gitops = True,
images = [
":external_image",
],
manifests = [
":deployment1.yaml",
],
namespace = "ci",
release_branch_prefix = "gitops_test_release_branch",
visibility = ["//visibility:public"],
)
golden_test(
name = "external_image_golden",
in_file = "external_image_test",
)
bzl_library(
name = "templates",
srcs = ["templates.bzl"],
visibility = ["//gitops:__subpackages__"],
)
================================================
FILE: gitops/private/test/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: //gitops/private/test/images:image
================================================
FILE: gitops/private/test/deployment1.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: //gitops/private/test:external_image
================================================
FILE: gitops/private/test/goldens/deploy_test.golden
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: ci
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- image: localhost:15000/rules_gitops/gitops/private/test/images/helloworld@sha256:c361bf07a58f30284effb92ee1c6504b75428c5019e8a60a27120038a20e9c3e
name: myapp
================================================
FILE: gitops/private/test/goldens/external_image_test.golden
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: ci
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- image: gcr.io/repo/someimage@sha:1234567890
name: myapp
================================================
FILE: gitops/private/test/images/BUILD.bazel
================================================
load("@rules_gitops//adapters:rules_img.bzl", "k8s_push_info")
load("@rules_img//img:image.bzl", "image_manifest")
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:load.bzl", "image_load")
load("@rules_img//img:push.bzl", "image_push")
package(
default_visibility = ["//gitops/private/test:__subpackages__"],
)
REGISTRY = "localhost:15000"
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
image_layer(
name = "layer",
srcs = {
"/bin/app": "container_content.txt",
},
)
image_manifest(
name = "image",
base = "@alpine",
entrypoint = ["/bin/app"],
layers = [
":layer",
],
platform = ":linux_amd64",
)
image_load(
name = "load",
image = ":image",
tag = "helloworld:latest",
visibility = ["//:__pkg__"],
)
image_push(
name = "push",
image = ":image",
registry = REGISTRY,
repository = "rules_gitops/gitops/private/test/images/helloworld",
tag = "native",
visibility = ["//:__pkg__"],
)
k8s_push_info(
name = "k8s_image",
image = ":image",
push = ":push",
registry = REGISTRY,
repository = "rules_gitops/gitops/private/test/images/helloworld",
)
================================================
FILE: gitops/private/test/images/container_content.txt
================================================
this file goes into the test container
================================================
FILE: gitops/private/test/templates.bzl
================================================
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules for templating and file layout."""
def _expand_template_impl(ctx):
"""Simply spawn the template-engine in a rule."""
arguments = [
"--template=%s" % ctx.file.template.path,
"--output=%s" % ctx.outputs.out.path,
]
stamps = [ctx.file._info_file]
for sf in stamps:
arguments.append("--stamp_info_file=%s" % sf.path)
for k in ctx.attr.substitutions:
arguments.append("--variable=%s=%s" % (k, ctx.attr.substitutions[k]))
if ctx.attr.start_tag:
arguments.append("--start_tag=%s" % ctx.attr.start_tag)
if ctx.attr.end_tag:
arguments.append("--end_tag=%s" % ctx.attr.end_tag)
if ctx.attr.executable:
arguments.append("--executable")
d = {
str(ctx.attr.deps[i].label): ctx.files.deps[i].path
for i in range(0, len(ctx.attr.deps))
}
arguments += ["--imports=%s=%s" % (k, d[k]) for k in d]
arguments += [
"--imports=%s=%s" % (k, d[str(ctx.label.relative(ctx.attr.deps_aliases[k]))])
for k in ctx.attr.deps_aliases
]
ctx.actions.run(
executable = ctx.executable._engine,
arguments = arguments,
inputs = [ctx.file.template] + ctx.files.deps + stamps,
outputs = [ctx.outputs.out],
mnemonic = "Template",
)
expand_template = rule(
doc = """
Expand a template file.
This rules expands the file given in template, into the file given by out.
Args:
template: The template file to expand.
deps: additional files to expand, they will be accessible as imports[label]
in the template environment. If a file ends with .tpl, it is considered
a template itself and will be expanded.
deps_aliases: a dictionary of name to label. Each label in that dictionary
should be present in the deps attribute, and will be make accessible as
imports[name] in the template environment.
substitutions: a dictionary of key => values that will appear as variables.key
in the template environment.
out: the name of the output file to generate.
executable: mark the result as excutable if set to True.
""",
attrs = {
"out": attr.output(mandatory = True),
"deps_aliases": attr.string_dict(default = {}),
"end_tag": attr.string(default = "}}"),
"executable": attr.bool(default = True),
# "escape_xml": attr.bool(default = True),
"start_tag": attr.string(default = "{{"),
"substitutions": attr.string_dict(mandatory = True),
"template": attr.label(
mandatory = True,
allow_single_file = True,
),
"deps": attr.label_list(default = [], allow_files = True),
"_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
},
implementation = _expand_template_impl,
)
def strip_prefix(path, prefixes):
for prefix in prefixes:
if path.startswith(prefix):
return path[len(prefix):]
return path
def strip_suffix(path, suffixes):
for suffix in suffixes:
if path.endswith(suffix):
return path[:-len(suffix)]
return path
def _dest_path(f, strip_prefixes, strip_suffixes):
"""Returns the short path of f, stripped of strip_prefixes and strip_suffixes."""
return strip_suffix(strip_prefix(f.short_path, strip_prefixes), strip_suffixes)
def _format_path(path_format, path):
dirsep = path.rfind("/")
dirname = path[:dirsep] if dirsep > 0 else ""
basename = path[dirsep + 1:] if dirsep > 0 else path
extsep = basename.rfind(".")
extension = basename[extsep + 1:] if extsep > 0 else ""
basename = basename[:extsep] if extsep > 0 else basename
return path_format.format(
path = path,
dirname = dirname,
basename = basename,
extension = extension,
)
def _append_inputs(args, inputs, f, path, path_format):
args.append("--file=%s=%s" % (
f.path,
_format_path(path_format, path),
))
inputs.append(f)
def _merge_files_impl(ctx):
"""Merge a list of config files in a tar ball with the correct layout."""
output = ctx.outputs.out
build_tar = ctx.executable._build_tar
inputs = []
args = [
"--output=" + output.path,
"--directory=" + ctx.attr.directory,
"--mode=0644",
]
variables = [
"--variable=%s=%s" % (k, ctx.attr.substitutions[k])
for k in ctx.attr.substitutions
]
for f in ctx.files.srcs:
path = _dest_path(f, ctx.attr.strip_prefixes, ctx.attr.strip_suffixes)
if path.endswith(ctx.attr.template_extension):
path = path[:-4]
f2 = ctx.actions.declare_file(ctx.label.name + "/" + path)
ctx.actions.run(
executable = ctx.executable._engine,
arguments = [
"--template=%s" % f.path,
"--output=%s" % f2.path,
"--noescape_xml",
] + variables,
inputs = [f],
outputs = [f2],
)
_append_inputs(args, inputs, f2, path, ctx.attr.path_format)
else:
_append_inputs(args, inputs, f, path, ctx.attr.path_format)
ctx.actions.run(
executable = build_tar,
arguments = args,
inputs = inputs,
outputs = [output],
mnemonic = "MergeFiles",
)
merge_files = rule(
doc = """
Merge a set of files in a single tarball.
This rule merge a set of files into one tarball, each file will appear in the
tarball as a file determined by path_format, strip_prefixes and directory.
Outputs:
.tar: the tarball containing all the files in srcs.
Args:
srcs: The list of files to merge. If a file is ending with ".tpl" (see
template_extension), it will get expanded like a template passed to
expand_template.
template_extension: extension of files to be considered as template, ".tpl"
by default.
directory: base directory for all the files in the resulting tarball.
strip_prefixes: list of prefixes to strip from the path of the srcs to obtain
the final path (see path_format).
strip_suffixes: list of suffixes to strip from the path of the srcs to obtain
the final path (see path_format).
substitutions: map of substitutions to make available during template
expansion. Values of that map will be available as "variables.name" in
the template environment.
path_format: format of the final files. Each file will appear in the final
tarball under "{directory}/{path_format}" where the following string of
path_format are replaced:
{path}: path of the input file, removed from prefixes and suffixes,
{dirname}: directory name of path,
{basename}: base filename of path,
{extension}: extension of path
""",
attrs = {
"srcs": attr.label_list(allow_files = True),
"directory": attr.string(default = "/"),
"path_format": attr.string(default = "{path}"),
"strip_prefixes": attr.string_list(default = []),
"strip_suffixes": attr.string_list(default = ["-staging", "-test"]),
"substitutions": attr.string_dict(default = {}),
"template_extension": attr.string(default = ".tpl"),
"_build_tar": attr.label(
default = Label("@bazel_tools//tools/build_defs/pkg:build_tar"),
cfg = "exec",
executable = True,
allow_files = True,
),
"_engine": attr.label(
cfg = "exec",
default = Label("//templating:template_engine"),
executable = True,
),
},
outputs = {"out": "%{name}.tar"},
implementation = _merge_files_impl,
)
================================================
FILE: go.mod
================================================
module github.com/adobe/rules_gitops
go 1.24.12
require (
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.7.0
github.com/google/go-github/v32 v32.1.0
github.com/xanzy/go-gitlab v0.80.2
golang.org/x/oauth2 v0.5.0
google.golang.org/protobuf v1.36.11
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
)
require github.com/golang/protobuf v1.5.2 // indirect
require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.90.0 // indirect
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kind v0.31.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
tool sigs.k8s.io/kind
================================================
FILE: go.sum
================================================
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/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/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xanzy/go-gitlab v0.80.2 h1:CH1Q7NDklqZllox4ICVF4PwlhQGfPtE+w08Jsb74ZX0=
github.com/xanzy/go-gitlab v0.80.2/go.mod h1:DlByVTSXhPsJMYL6+cm8e8fTJjeBmhrXdC/yvkKKt6M=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
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-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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sys v0.0.0-20180830151530-49385e6e1522/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-20200323222414-85ca7c5b95cd/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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/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=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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/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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ=
k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg=
k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M=
k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d h1:oFDpQ7FfzinCtrFOl4izwOWsdTprlS2A9IXBENMW0UA=
k8s.io/kube-openapi v0.0.0-20230217203603-ff9a8e8fa21d/go.mod h1:/BYxry62FuDzmI+i9B+X2pqfySRmSOW2ARmj5Zbqhj0=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g=
sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
================================================
FILE: kubectl/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load(":defs.bzl", "resolved_toolchain")
toolchain_type(
name = "toolchain_type",
visibility = ["//visibility:public"],
)
resolved_toolchain(
name = "resolved_toolchain",
visibility = ["//visibility:public"],
)
bzl_library(
name = "defs",
srcs = ["defs.bzl"],
visibility = ["//visibility:public"],
deps = [
"//kubectl/private:extension",
"//kubectl/private:kubectl_binary",
"//kubectl/private:resolved_toolchain",
"//kubectl/private:toolchain",
],
)
================================================
FILE: kubectl/defs.bzl
================================================
"""Public API for kubectl rules."""
load("//kubectl/private:extension.bzl", _kubectl = "kubectl")
load("//kubectl/private:kubectl_binary.bzl", _kubectl_binary = "kubectl_binary")
load("//kubectl/private:resolved_toolchain.bzl", _resolved_toolchain = "resolved_toolchain")
load("//kubectl/private:toolchain.bzl", _kubectl_toolchain = "kubectl_toolchain")
kubectl = _kubectl
kubectl_binary = _kubectl_binary
kubectl_toolchain = _kubectl_toolchain
resolved_toolchain = _resolved_toolchain
================================================
FILE: kubectl/private/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
package(
default_visibility = ["//kubectl:__subpackages__"],
)
exports_files([
"run-all.sh.tpl",
])
bzl_library(
name = "extension",
srcs = ["extension.bzl"],
deps = [
":platforms",
"//kubectl/private/versions",
"@bazel_tools//tools/build_defs/repo:cache.bzl",
"@bazel_tools//tools/build_defs/repo:http.bzl",
"@bazel_tools//tools/build_defs/repo:utils.bzl",
],
)
bzl_library(
name = "toolchain",
srcs = ["toolchain.bzl"],
deps = [":providers"],
)
bzl_library(
name = "kubectl_binary",
srcs = ["kubectl_binary.bzl"],
deps = [
"//adapters:providers",
"//kustomize:defs",
"//stamper:stamp",
],
)
bzl_library(
name = "kubeconfig",
srcs = ["kubeconfig.bzl"],
)
bzl_library(
name = "platforms",
srcs = ["platforms.bzl"],
)
bzl_library(
name = "providers",
srcs = ["providers.bzl"],
)
bzl_library(
name = "resolved_toolchain",
srcs = ["resolved_toolchain.bzl"],
)
================================================
FILE: kubectl/private/extension.bzl
================================================
"""Module extension for kubectl toolchain."""
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
load("//kubectl/private:platforms.bzl", "PLATFORMS")
load("//kubectl/private/versions:versions.bzl", "LATEST_KUBECTL_VERSION", "VERSIONS")
def _kubectl_hub_impl(rctx):
kubectl_hub_build_content = """
package(
default_visibility = ["//visibility:public"]
)
"""
toolchains_build_content = """load("@rules_gitops//kubectl:defs.bzl", "kubectl_toolchain")"""
for [platform, meta] in PLATFORMS.items():
toolchains_build_content += """
kubectl_toolchain(
name = "kubectl_{platform}_toolchain",
executable = "@kubectl_{platform}//file"
)
toolchain(
name = "{platform}_toolchain",
exec_compatible_with = {compatible_with},
toolchain = ":kubectl_{platform}_toolchain",
toolchain_type = "@rules_gitops//kubectl:toolchain_type",
)
""".format(
platform = platform,
compatible_with = meta.compatible_with,
)
rctx.file("toolchains/BUILD.bazel", content = toolchains_build_content)
rctx.file("BUILD.bazel", content = kubectl_hub_build_content)
_kubectl_hub = repository_rule(
implementation = _kubectl_hub_impl,
attrs = {},
)
KUBECTL_TOOLCHAIN_BUILD = """
kubectl_toolchain(
name = "kubectl_toolchain",
executable = "file"
)
"""
def _kubectl_extension_impl(module_ctx):
kubectl_version = LATEST_KUBECTL_VERSION
for mod in module_ctx.modules:
if len(mod.tags.toolchain) > 1:
fail("Expected kubectl toolchain to only be declared once")
# Allow the root module to override the kubectl toolchain version
if mod.is_root and len(mod.tags.toolchain) == 1:
kubectl_version = mod.tags.toolchain[0].version
if not VERSIONS[kubectl_version]:
fail("No matching version found for kubectl v{}".format(kubectl_version))
binaries = VERSIONS[kubectl_version]
for [platform, sha256] in binaries.items():
[os, arch] = platform.split("_")
http_file(
name = "kubectl_{}".format(platform),
urls = [
"https://dl.k8s.io/release/v{version}/bin/{os}/{arch}/kubectl".format(version = kubectl_version, os = os, arch = arch),
],
executable = True,
sha256 = sha256,
)
_kubectl_hub(
name = "kubectl",
)
return module_ctx.extension_metadata()
kubectl = module_extension(
implementation = _kubectl_extension_impl,
tag_classes = {
"toolchain": tag_class(
attrs = {
"version": attr.string(
doc = "kubectl binary version",
),
},
),
},
)
================================================
FILE: kubectl/private/kubeconfig.bzl
================================================
"""Repository rule for configuring kubectl and kubeconfig."""
def _kubectl_config(repository_ctx, args):
kubectl = repository_ctx.path("kubectl")
kubeconfig_yaml = repository_ctx.path("kubeconfig")
exec_result = repository_ctx.execute(
[kubectl, "--kubeconfig", kubeconfig_yaml, "config"] + args,
environment = {
# prevent kubectl config to stumble on shared .kube/config.lock file
"HOME": str(repository_ctx.path(".")),
},
quiet = True,
)
if exec_result.return_code != 0:
fail("Error executing kubectl config %s" % " ".join(args))
def _kubeconfig_impl(repository_ctx):
"""Find local kubernetes certificates"""
# find and symlink kubectl
kubectl = repository_ctx.which("kubectl")
if not kubectl:
fail("Unable to find kubectl executable. PATH=%s" % repository_ctx.path)
repository_ctx.symlink(kubectl, "kubectl")
repository_ctx.file(repository_ctx.path("cluster"), content = repository_ctx.attr.cluster, executable = False)
# TODO: figure out how to use BUILD_USER
if "USER" in repository_ctx.os.environ:
user = repository_ctx.os.environ["USER"]
else:
exec_result = repository_ctx.execute(["whoami"])
if exec_result.return_code != 0:
fail("Error detecting current user")
user = exec_result.stdout.rstrip()
token = None
ca_crt = None
kubecert_cert = None
kubecert_key = None
server = repository_ctx.attr.server
# check service account first
serviceaccount = repository_ctx.path("/var/run/secrets/kubernetes.io/serviceaccount")
if serviceaccount.exists:
ca_crt = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
token_file = serviceaccount.get_child("token")
if token_file.exists:
exec_result = repository_ctx.execute(["cat", token_file.realpath])
if exec_result.return_code != 0:
fail("Error reading user token")
token = exec_result.stdout.rstrip()
# use master url from the environemnt
if "KUBERNETES_SERVICE_HOST" in repository_ctx.os.environ:
server = "https://%s:%s" % (
repository_ctx.os.environ["KUBERNETES_SERVICE_HOST"],
repository_ctx.os.environ["KUBERNETES_SERVICE_PORT"],
)
else:
# fall back to the default
server = "https://kubernetes.default"
elif repository_ctx.attr.use_host_config:
home = repository_ctx.path(repository_ctx.os.environ["HOME"])
kubeconfig = home.get_child(".kube").get_child("config")
if repository_ctx.path(kubeconfig).exists:
repository_ctx.symlink(kubeconfig, repository_ctx.path("kubeconfig"))
else:
_kubectl_config(repository_ctx, [
"set-cluster",
repository_ctx.attr.cluster,
"--server",
server,
])
else:
home = repository_ctx.path(repository_ctx.os.environ["HOME"])
certs = home.get_child(".kube").get_child("certs")
ca_crt = certs.get_child("ca.crt").realpath
kubecert_cert = certs.get_child("kubecert.cert")
kubecert_key = certs.get_child("kubecert.key")
# config set-cluster {cluster} \
# --certificate-authority=... \
# --server=https://dev3.k8s.tubemogul.info:443 \
# --embed-certs",
if ca_crt:
_kubectl_config(repository_ctx, [
"set-cluster",
repository_ctx.attr.cluster,
"--server",
server,
"--certificate-authority",
ca_crt,
])
# config set-credentials {user} --token=...",
if token:
_kubectl_config(repository_ctx, [
"set-credentials",
user,
"--token",
token,
])
# config set-credentials {user} --client-certificate=... --embed-certs",
if kubecert_cert and kubecert_cert.exists:
_kubectl_config(repository_ctx, [
"set-credentials",
user,
"--client-certificate",
kubecert_cert.realpath,
])
# config set-credentials {user} --client-key=... --embed-certs",
if kubecert_key and kubecert_key.exists:
_kubectl_config(repository_ctx, [
"set-credentials",
user,
"--client-key",
kubecert_key.realpath,
])
# export repostory contents
repository_ctx.file("BUILD", """exports_files(["kubeconfig", "kubectl", "cluster"])""", False)
return {
"name": repository_ctx.attr.name,
"cluster": repository_ctx.attr.cluster,
"server": repository_ctx.attr.server,
"use_host_config": repository_ctx.attr.use_host_config,
}
kubeconfig = repository_rule(
attrs = {
"cluster": attr.string(),
"server": attr.string(),
"use_host_config": attr.bool(),
},
environ = [
"HOME",
"USER",
"KUBERNETES_SERVICE_HOST",
"KUBERNETES_SERVICE_PORT",
],
local = True,
implementation = _kubeconfig_impl,
)
================================================
FILE: kubectl/private/kubectl_binary.bzl
================================================
"""
Simple rule for running kubectl from the toolchain config
"""
load("//adapters:providers.bzl", "K8sPushInfo")
load("//kustomize:defs.bzl", "KustomizeInfo")
load("//stamper:stamp.bzl", "stamp")
def _kubectl_binary_impl(ctx):
executable = ctx.toolchains["@rules_gitops//kubectl:toolchain_type"].kubectlinfo.executable
runfiles = ctx.runfiles(
files = ctx.files.srcs + [ctx.executable._template_engine, ctx.file._info_file, executable],
).merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles)
files = []
cluster_arg = ctx.attr.cluster
cluster_arg = ctx.expand_make_variables("cluster", cluster_arg, {})
if "{" in ctx.attr.cluster:
cluster_arg, cluster_files = stamp(ctx, cluster_arg, ctx.files.srcs, ctx.label.name + ".cluster-name")
files.extend(cluster_files)
user_arg = ctx.attr.user
user_arg = ctx.expand_make_variables("user", user_arg, {})
if "{" in ctx.attr.user:
user_arg, user_files = stamp(ctx, user_arg, ctx.files.srcs, ctx.label.name + ".user-name")
files.extend(user_files)
kubectl_command_arg = ctx.attr.command
kubectl_command_arg = ctx.expand_make_variables("kubectl_command", kubectl_command_arg, {})
statements = ""
transitive_runfiles = []
if ctx.attr.push:
trans_img_pushes = depset(transitive = [obj[KustomizeInfo].image_pushes for obj in ctx.attr.srcs]).to_list()
statements += "\n".join([
"# {}\n".format(exe[K8sPushInfo].image_label) +
"echo pushing {}/{}".format(exe[K8sPushInfo].registry, exe[K8sPushInfo].repository)
for exe in trans_img_pushes
if hasattr(exe[K8sPushInfo], "pusher")
]) + "\n"
statements += "\n".join([
"async \"%s\"" % exe[K8sPushInfo].pusher.files_to_run.executable.short_path
for exe in trans_img_pushes
if hasattr(exe[K8sPushInfo], "pusher")
]) + "\nwaitpids\n"
transitive_runfiles += [exe[K8sPushInfo].pusher.default_runfiles for exe in trans_img_pushes if hasattr(exe[K8sPushInfo], "pusher")]
runfiles = runfiles.merge_all(transitive_runfiles)
namespace = ctx.attr.namespace
for inattr in ctx.attr.srcs:
for infile in inattr.files.to_list():
statements += "{template_engine} --template={infile} --variable=NAMESPACE={namespace} --stamp_info_file={info_file} | \"{kubectl}\" --cluster=\"{cluster}\" --user=\"{user}\" {kubectl_command} -f -\n".format(
kubectl = executable.short_path,
infile = infile.short_path,
cluster = cluster_arg,
user = user_arg,
kubectl_command = kubectl_command_arg,
template_engine = ctx.executable._template_engine.short_path,
namespace = namespace,
info_file = ctx.file._info_file.short_path,
)
ctx.actions.expand_template(
template = ctx.file._template,
substitutions = {
"%{statements}": statements,
},
output = ctx.outputs.executable,
)
files.append(ctx.outputs.executable)
return [
DefaultInfo(files = depset(files), runfiles = runfiles, executable = ctx.outputs.executable),
]
kubectl_binary = rule(
attrs = {
"srcs": attr.label_list(providers = [KustomizeInfo]),
"cluster": attr.string(mandatory = True),
"namespace": attr.string(mandatory = True),
"command": attr.string(default = "apply"),
"user": attr.string(default = "{BUILD_USER}"),
"push": attr.bool(default = True),
"_build_user_value": attr.label(
default = Label("//stamper:build_user_value.txt"),
allow_single_file = True,
),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
"_stamper": attr.label(
default = Label("//stamper:stamper"),
cfg = "exec",
executable = True,
allow_files = True,
),
"_template": attr.label(
default = Label("//kubectl/private:run-all.sh.tpl"),
allow_single_file = True,
),
"_template_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
"_bash_runfiles": attr.label(
default = Label("@bazel_tools//tools/bash/runfiles"),
),
},
executable = True,
implementation = _kubectl_binary_impl,
toolchains = ["@rules_gitops//kubectl:toolchain_type"],
)
================================================
FILE: kubectl/private/platforms.bzl
================================================
"""Platform definitions for kubectl toolchain."""
PLATFORMS = {
"darwin_amd64": struct(
compatible_with = [
"@platforms//os:macos",
"@platforms//cpu:x86_64",
],
),
"darwin_arm64": struct(
compatible_with = [
"@platforms//os:macos",
"@platforms//cpu:arm64",
],
),
"linux_amd64": struct(
compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
),
"linux_arm64": struct(
compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
),
}
================================================
FILE: kubectl/private/providers.bzl
================================================
"""Provider definitions for kubectl toolchain."""
KubectlToolchainInfo = provider(
doc = "Toolchain information about the kubectl executable",
fields = {
"target_tool_path": "Path to the kubectl executable for the target platform.",
"executable": "Hermetically download toolchain executable file",
},
)
================================================
FILE: kubectl/private/resolved_toolchain.bzl
================================================
"""This module implements an alias rule to the resolved toolchain.
"""
DOC = """\
Exposes a concrete toolchain which is the result of Bazel resolving the
toolchain for the execution or target platform.
Workaround for https://github.com/bazelbuild/bazel/issues/14009
"""
# Forward all the providers
def _resolved_toolchain_impl(ctx):
toolchain_info = ctx.toolchains["//kubectl:toolchain_type"]
return [
toolchain_info,
toolchain_info.default,
toolchain_info.kubectlinfo,
toolchain_info.template_variables,
]
# Copied from java_toolchain_alias
# https://cs.opensource.google/bazel/bazel/+/master:tools/jdk/java_toolchain_alias.bzl
resolved_toolchain = rule(
implementation = _resolved_toolchain_impl,
toolchains = ["//kubectl:toolchain_type"],
doc = DOC,
)
================================================
FILE: kubectl/private/run-all.sh.tpl
================================================
#!/usr/bin/env bash
set -eu
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
PIDS=()
function async() {
# Launch the command asynchronously and track its process id.
"./$@" &
PIDS+=($!)
}
function waitpids() {
# Wait for all of the subprocesses, failing the script if any of them failed.
if [ "${#PIDS[@]}" != 0 ]; then
for pid in ${PIDS[@]}; do
wait ${pid}
done
fi
}
%{statements}
================================================
FILE: kubectl/private/toolchain.bzl
================================================
"Kubectl toolchain rule"
load("//kubectl/private:providers.bzl", "KubectlToolchainInfo")
# Avoid using non-normalized paths (workspace/../other_workspace/path)
def _to_manifest_path(ctx, file):
if file.short_path.startswith("../"):
return "external/" + file.short_path[3:]
else:
return ctx.workspace_name + "/" + file.short_path
def _kubectl_toolchain_impl(ctx):
executable = ctx.file.executable
target_tool_path = _to_manifest_path(ctx, executable)
template_variables = platform_common.TemplateVariableInfo({
"KUBECTL_BIN": target_tool_path,
})
default = DefaultInfo(
files = depset([executable]),
runfiles = ctx.runfiles(files = [executable]),
)
kubectlinfo = KubectlToolchainInfo(
target_tool_path = target_tool_path,
executable = executable,
)
toolchain_info = platform_common.ToolchainInfo(
kubectlinfo = kubectlinfo,
template_variables = template_variables,
default = default,
)
return [
default,
toolchain_info,
template_variables,
]
kubectl_toolchain = rule(
implementation = _kubectl_toolchain_impl,
attrs = {
"executable": attr.label(
doc = "A hermetically downloaded executable target for the target platform.",
mandatory = True,
allow_single_file = True,
),
},
)
================================================
FILE: kubectl/private/versions/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["update_versions.go"],
importpath = "github.com/adobe/rules_gitops/kubectl/private/versions",
visibility = ["//visibility:private"],
)
go_binary(
name = "update_versions",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
bzl_library(
name = "versions",
srcs = ["versions.bzl"],
visibility = ["//kubectl:__subpackages__"],
)
================================================
FILE: kubectl/private/versions/update_versions.go
================================================
// update_versions downloads kubectl release information from dl.k8s.io
// and generates a versions.bzl file with SHA256 digests for each platform.
//
// Usage:
//
// go run update_versions.go
//
// The script expects BUILD_WORKSPACE_DIRECTORY to be set, or writes to
// the default location relative to this file's directory.
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
const (
dlBaseURL = "https://dl.k8s.io/release/"
outputFile = "kubectl/private/versions/versions.bzl"
)
// Platform combinations to fetch
var platforms = []struct {
os string
arch string
}{
{"darwin", "amd64"},
{"darwin", "arm64"},
{"linux", "amd64"},
{"linux", "arm64"},
}
// VersionInfo holds platform -> sha256 mappings for a version
type VersionInfo struct {
Platforms map[string]string // platform (e.g., "darwin_arm64") -> sha256
}
func main() {
log.SetFlags(0)
// Discover stable versions
versions := discoverStableVersions()
if len(versions) == 0 {
log.Fatalf("No stable versions found")
}
log.Printf("Found %d stable versions: %v", len(versions), versions)
// Fetch SHA256 checksums for each version and platform
versionInfos := fetchAllChecksums(versions)
// Find latest version
latestVersion := findLatestVersion(versions)
// Generate versions.bzl content
content := generateBzl(versionInfos, latestVersion)
// Determine output path
outputPath := getOutputPath()
// Write the file
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
log.Fatalf("Failed to create directory: %v", err)
}
if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
log.Fatalf("Failed to write file: %v", err)
}
log.Printf("Successfully wrote %s with %d versions", outputPath, len(versionInfos))
}
// discoverStableVersions probes stable-X.Y.txt endpoints to find available versions
func discoverStableVersions() []string {
var versions []string
// First, get the absolute latest stable to determine the max minor version
latestStable := fetchStableVersion("stable.txt")
if latestStable == "" {
log.Printf("Warning: could not fetch stable.txt")
return versions
}
versions = append(versions, latestStable)
// Parse the minor version from latest stable (e.g., "1.35.0" -> 35)
maxMinor := parseMinorVersion(latestStable)
if maxMinor == 0 {
log.Printf("Warning: could not parse minor version from %s", latestStable)
return versions
}
log.Printf("Latest stable is %s (minor version %d)", latestStable, maxMinor)
// Probe stable-1.X.txt for minor versions from 20 up to the latest
// Kubernetes typically maintains ~3-4 minor versions, but we fetch more for completeness
for minor := 20; minor <= maxMinor; minor++ {
filename := fmt.Sprintf("stable-1.%d.txt", minor)
if v := fetchStableVersion(filename); v != "" {
// Avoid duplicates
found := false
for _, existing := range versions {
if existing == v {
found = true
break
}
}
if !found {
versions = append(versions, v)
}
}
}
return versions
}
// parseMinorVersion extracts the minor version number from a version string like "1.35.0"
func parseMinorVersion(version string) int {
parts := strings.Split(version, ".")
if len(parts) < 2 {
return 0
}
var minor int
fmt.Sscanf(parts[1], "%d", &minor)
return minor
}
// fetchStableVersion fetches a stable version string from dl.k8s.io
func fetchStableVersion(filename string) string {
url := dlBaseURL + filename
resp, err := http.Get(url)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}
version := strings.TrimSpace(string(body))
// Validate it looks like a version (starts with 'v')
if !strings.HasPrefix(version, "v") {
return ""
}
// Strip the 'v' prefix for consistency
return strings.TrimPrefix(version, "v")
}
// fetchAllChecksums fetches SHA256 checksums for all versions and platforms
func fetchAllChecksums(versions []string) map[string]*VersionInfo {
result := make(map[string]*VersionInfo)
var mu sync.Mutex
var wg sync.WaitGroup
// Use a semaphore to limit concurrent requests
sem := make(chan struct{}, 10)
for _, version := range versions {
for _, p := range platforms {
wg.Add(1)
go func(version, os, arch string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
sha256, err := fetchSHA256(version, os, arch)
if err != nil {
log.Printf("Warning: failed to fetch sha256 for %s %s/%s: %v", version, os, arch, err)
return
}
platform := fmt.Sprintf("%s_%s", os, arch)
mu.Lock()
if result[version] == nil {
result[version] = &VersionInfo{
Platforms: make(map[string]string),
}
}
result[version].Platforms[platform] = sha256
mu.Unlock()
}(version, p.os, p.arch)
}
}
wg.Wait()
return result
}
// fetchSHA256 fetches the SHA256 checksum for a specific version/os/arch
func fetchSHA256(version, os, arch string) (string, error) {
url := fmt.Sprintf("%sv%s/bin/%s/%s/kubectl.sha256", dlBaseURL, version, os, arch)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// The sha256 file may contain just the hash, or "hash filename" format
content := strings.TrimSpace(string(body))
parts := strings.Fields(content)
if len(parts) == 0 {
return "", fmt.Errorf("empty sha256 file")
}
sha256 := parts[0]
// Validate it looks like a SHA256 (64 hex chars)
if len(sha256) != 64 {
return "", fmt.Errorf("invalid sha256 length: %d", len(sha256))
}
return sha256, nil
}
// findLatestVersion returns the latest version from a list of versions
func findLatestVersion(versions []string) string {
if len(versions) == 0 {
return ""
}
latest := versions[0]
for _, v := range versions[1:] {
if compareVersions(v, latest) > 0 {
latest = v
}
}
return latest
}
// compareVersions compares two semver-like version strings
// Returns positive if v1 > v2, negative if v1 < v2, zero if equal
func compareVersions(v1, v2 string) int {
if v2 == "" {
return 1
}
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
for i := 0; i < len(parts1) && i < len(parts2); i++ {
var n1, n2 int
fmt.Sscanf(parts1[i], "%d", &n1)
fmt.Sscanf(parts2[i], "%d", &n2)
if n1 != n2 {
return n1 - n2
}
}
return len(parts1) - len(parts2)
}
func generateBzl(versions map[string]*VersionInfo, latestVersion string) string {
var sb strings.Builder
// Sort versions for consistent output (reverse to put newer versions first)
versionList := make([]string, 0, len(versions))
for v := range versions {
versionList = append(versionList, v)
}
sort.Slice(versionList, func(i, j int) bool {
return compareVersions(versionList[i], versionList[j]) > 0
})
sb.WriteString(`"""Generated by update_versions.go - do not edit manually."""
`)
sb.WriteString(fmt.Sprintf("LATEST_KUBECTL_VERSION = %q\n\n", latestVersion))
sb.WriteString("VERSIONS = {\n")
for _, version := range versionList {
info := versions[version]
sb.WriteString(fmt.Sprintf(" %q: {\n", version))
// Sort platforms for consistent output
platformKeys := make([]string, 0, len(info.Platforms))
for p := range info.Platforms {
platformKeys = append(platformKeys, p)
}
sort.Strings(platformKeys)
for _, platform := range platformKeys {
sha256 := info.Platforms[platform]
sb.WriteString(fmt.Sprintf(" %q: %q,\n", platform, sha256))
}
sb.WriteString(" },\n")
}
sb.WriteString("}\n")
return sb.String()
}
func getOutputPath() string {
// Check for BUILD_WORKSPACE_DIRECTORY environment variable
if wsDir := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); wsDir != "" {
return filepath.Join(wsDir, outputFile)
}
// Fallback: find workspace root by looking for MODULE.bazel or WORKSPACE
// Start from executable directory and walk up
exePath, err := os.Executable()
if err != nil {
log.Fatalf("Failed to get executable path: %v", err)
}
dir := filepath.Dir(exePath)
// Also try current working directory
cwd, _ := os.Getwd()
for _, startDir := range []string{dir, cwd} {
d := startDir
for d != "/" && d != "." {
if _, err := os.Stat(filepath.Join(d, "MODULE.bazel")); err == nil {
return filepath.Join(d, outputFile)
}
if _, err := os.Stat(filepath.Join(d, "WORKSPACE")); err == nil {
return filepath.Join(d, outputFile)
}
d = filepath.Dir(d)
}
}
log.Fatalf("Could not find workspace root. Set BUILD_WORKSPACE_DIRECTORY or run from within the workspace.")
return ""
}
================================================
FILE: kubectl/private/versions/versions.bzl
================================================
"""Generated by update_versions.go - do not edit manually."""
LATEST_KUBECTL_VERSION = "1.35.0"
VERSIONS = {
"1.35.0": {
"darwin_amd64": "2447cb78911b10a667202b078eeb30541ec78d1280c3682921dc81607e148d96",
"darwin_arm64": "cf699c56340dc775230fde4ef84237d27563ea6ef52164c7d078072b586c3918",
"linux_amd64": "a2e984a18a0c063279d692533031c1eff93a262afcc0afdc517375432d060989",
"linux_arm64": "58f82f9fe796c375c5c4b8439850b0f3f4d401a52434052f2df46035a8789e25",
},
"1.34.3": {
"darwin_amd64": "657afbd0e653c4ce3af1b5a645a4eaba282cf8eb2bcda7191ff60866e50e4d7f",
"darwin_arm64": "e51367d2107d605f4edd7c2fb25897b0c0695a7de1a9f9d04cd6c9356b890b14",
"linux_amd64": "ab60ca5f0fd60c1eb81b52909e67060e3ba0bd27e55a8ac147cbc2172ff14212",
"linux_arm64": "46913a7aa0327f6cc2e1cc2775d53c4a2af5e52f7fd8dacbfbfd098e757f19e9",
},
"1.33.7": {
"darwin_amd64": "45be3f5293da84d97e86580a541b247fe3cec60196fdd6abd2b811d7dd4d3f1b",
"darwin_arm64": "2e333f56d115081af83a48b5f31a91fb32852550f8117a0a31cf8bae2e601704",
"linux_amd64": "471d94e208a89be62eb776700fc8206cbef11116a8de2dc06fc0086b0015375b",
"linux_arm64": "fa7ee98fdb6fba92ae05b5e0cde0abd5972b2d9a4a084f7052a1fd0dce6bc1de",
},
"1.32.11": {
"darwin_amd64": "8d0b610df71632d0e9b9c1aa16dde5ec666c05bf24e401ecf20fd27af16879ad",
"darwin_arm64": "a39978a062f0df17d4a5551bd2e3a91eda90039196653935c50140be547141d3",
"linux_amd64": "48581d0e808bd8b7d3c3fc014e86b170e25a987df04c8a879b982b28a5180815",
"linux_arm64": "b1c91c106ec20e61c5dff869e9a39e6af4fb96572bddaac9cce307dfa3ed2348",
},
"1.31.14": {
"darwin_amd64": "efcaa4da4ff8d1a4d08e972f24c06ab99de2f16bd91c436fd602a275b73d3b78",
"darwin_arm64": "ee72e65cc206edc9b0d878e6d896fa31683dc1ba4c04944984efa07eebfc4725",
"linux_amd64": "8791ec7c8966b61420d55103a5fb948de9f0ca3d7306d789734975ad9704bdb0",
"linux_arm64": "3abb0c2d7121e1833831f56fd857a93de386e76d14b64baf86220d0afe495209",
},
"1.30.14": {
"darwin_amd64": "9d9e5062fdba88b45f57d8fe322545d129cc5ef1e572f7f4f0c4b6579162b7da",
"darwin_arm64": "af47a6a5f25630aa2bd68e56d21110b46aa1595a2d99f936637c75bc6bb65016",
"linux_amd64": "7ccac981ece0098284d8961973295f5124d78eab7b89ba5023f35591baa16271",
"linux_arm64": "a32e46ae15fe41292dc6a7cd76beba7104282a5a3fa9e3686319000a537f4f5d",
},
"1.29.15": {
"darwin_amd64": "fe172c614d423b31d57dca699f1c3990042bfb90768512814d2614dce4563f15",
"darwin_arm64": "1505a09992579bdc94565d872d840c97acfa84cc0478302491b58a754d4de5d6",
"linux_amd64": "3473e14c7b024a6e5403c6401b273b3faff8e5b1fed022d633815eb3168e4516",
"linux_arm64": "a41984dc0ff34ee05f1283ebd9b3121c003b3469b97214738246faa5b6788f7c",
},
"1.28.15": {
"darwin_amd64": "3180c84131002037d60fe7322794c20297d0e1b1514eaea20e33f77a00d8f2f4",
"darwin_arm64": "06a276bdb6da95af148d589f6c983ec8ea10c38f277ced6d97123938c8146078",
"linux_amd64": "1f7651ad0b50ef4561aa82e77f3ad06599b5e6b0b2a5fb6c4f474d95a77e41c5",
"linux_arm64": "7d45d9620e67095be41403ed80765fe47fcfbf4b4ed0bf0d1c8fe80345bda7d3",
},
"1.27.16": {
"darwin_amd64": "8d7f339660ba9b33ed56d540bed41b37babc945975a9e7027010697249b9ac5a",
"darwin_arm64": "d6bc47098bcb13a0ff5c267b30021b499aff4d960bd92610c2b0bc6f6e7246c9",
"linux_amd64": "97ea7cd771d0c6e3332614668a40d2c5996f0053ff11b44b198ea84dba0818cb",
"linux_arm64": "2f50cb29d73f696ffb57437d3e2c95b22c54f019de1dba19e2b834e0b4501eb9",
},
"1.26.15": {
"darwin_amd64": "ad4e980f9c304840ec9227a78a998e132ea23f3ca1bc0df7718ed160341bad0b",
"darwin_arm64": "c20b920d7e8e3ce3209c7c109fcfc4c09ad599613bc04b72c3f70d9fee598b68",
"linux_amd64": "b75f359e6fad3cdbf05a0ee9d5872c43383683bb8527a9e078bb5b8a44350a41",
"linux_arm64": "1396313f0f8e84ab1879757797992f1af043e1050283532e0fd8469902632216",
},
"1.25.16": {
"darwin_amd64": "34e87fdf0613502edbd2a2b00de5ee8c7789ab10e33257d14423dc6879321920",
"darwin_arm64": "d364f73df218b02642d06f3fa9b7345d64c03567b96ca21d361b487f48a33ccc",
"linux_amd64": "5a9bc1d3ebfc7f6f812042d5f97b82730f2bdda47634b67bddf36ed23819ab17",
"linux_arm64": "d6c23c80828092f028476743638a091f2f5e8141273d5228bf06c6671ef46924",
},
"1.24.17": {
"darwin_amd64": "1eb904b2c1148ff8431b0bd86677287a48bff000f93fd2d36377fbe956bd1e49",
"darwin_arm64": "7addbe3f1e22a366fa05aed4f268e77e83d902b40a5854e192b4205ed92e5f8d",
"linux_amd64": "3e9588e3326c7110a163103fc3ea101bb0e85f4d6fd228cf928fa9a2a20594d5",
"linux_arm64": "66885bda3a202546778c77f0b66dcf7f576b5a49ff9456acf61329da784a602d",
},
"1.23.17": {
"darwin_amd64": "7ece6543e3ca2ae9698ef61bbb2a4e249aa21319df4ea1b27c136a9b005dd7d8",
"darwin_arm64": "3b4590d67b31e3a94a9633064571c981907555da5376c34960cddfcd552f6114",
"linux_amd64": "f09f7338b5a677f17a9443796c648d2b80feaec9d6a094ab79a77c8a01fde941",
"linux_arm64": "c4a48fdc6038beacbc5de3e4cf6c23639b643e76656aabe2b7798d3898ec7f05",
},
"1.22.17": {
"darwin_amd64": "c3b8ae5ad48e1e126b5db2e7e22bb1e6ac54901a7f94ce499d12316f705e5e15",
"darwin_arm64": "b2d881bd6d3c688645cbc9e5b4cf4fe8945e1cfc3f2c07c795d2ee605ce4e568",
"linux_amd64": "7506a0ae7a59b35089853e1da2b0b9ac0258c5309ea3d165c3412904a9051d48",
"linux_arm64": "8fc2f8d5c80a6bf60be06f8cf28679a05ce565ce0bc81e70aaac38e0f7da6259",
},
"1.21.14": {
"darwin_amd64": "30c529fe2891eb93dda99597b5c84cb10d2318bb92ae89e1e6189b3ae5fb6296",
"darwin_arm64": "e0e6e413e19abc9deb15f9bd3c72f73ff5539973758e64ebca0f5eb085de6a00",
"linux_amd64": "0c1682493c2abd7bc5fe4ddcdb0b6e5d417aa7e067994ffeca964163a988c6ee",
"linux_arm64": "a23151bca5d918e9238546e7af416422b51cda597a22abaae5ca50369abfbbaa",
},
"1.20.15": {
"darwin_amd64": "6b6cf555a34271379b45013dfa9b580329314254aafc91b543bf2d83ebd1db74",
"linux_amd64": "d283552d3ef3b0fd47c08953414e1e73897a1b3f88c8a520bb2e7de4e37e96f3",
"linux_arm64": "d479febfb2e967bd86240b5c0b841e40e39e1ef610afd6f224281a23318c13dc",
},
}
================================================
FILE: kustomize/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("//kustomize/private:resolved_toolchain.bzl", "resolved_toolchain")
toolchain_type(
name = "toolchain_type",
visibility = ["//visibility:public"],
)
resolved_toolchain(
name = "resolved_toolchain",
visibility = ["//visibility:public"],
)
bzl_library(
name = "defs",
srcs = ["defs.bzl"],
visibility = ["//visibility:public"],
deps = [
"//kustomize/private:extension",
"//kustomize/private:kustomization",
"//kustomize/private:kustomize_binary",
"//kustomize/private:show",
"//kustomize/private:toolchain",
],
)
================================================
FILE: kustomize/defs.bzl
================================================
"""Public API for kustomize rules."""
load("//kustomize/private:extension.bzl", _kustomize = "kustomize")
load("//kustomize/private:kustomization.bzl", _kustomization = "kustomization")
load("//kustomize/private:kustomize_binary.bzl", _kustomize_binary = "kustomize_binary")
load("//kustomize/private:providers.bzl", _KustomizeInfo = "KustomizeInfo")
load("//kustomize/private:show.bzl", _show = "show")
load("//kustomize/private:toolchain.bzl", _kustomize_toolchain = "kustomize_toolchain")
KustomizeInfo = _KustomizeInfo
kustomize = _kustomize
kustomization = _kustomization
kustomize_binary = _kustomize_binary
kustomize_toolchain = _kustomize_toolchain
show = _show
================================================
FILE: kustomize/private/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
package(
default_visibility = ["//kustomize:__subpackages__"],
)
bzl_library(
name = "extension",
srcs = ["extension.bzl"],
deps = [
":platforms",
"//kustomize/private/versions",
"@bazel_tools//tools/build_defs/repo:cache.bzl",
"@bazel_tools//tools/build_defs/repo:http.bzl",
"@bazel_tools//tools/build_defs/repo:utils.bzl",
],
)
bzl_library(
name = "kustomization",
srcs = ["kustomization.bzl"],
deps = [
":providers",
"//adapters:providers",
"//stamper:stamp",
],
)
bzl_library(
name = "toolchain",
srcs = ["toolchain.bzl"],
deps = [":providers"],
)
bzl_library(
name = "kustomize_binary",
srcs = ["kustomize_binary.bzl"],
)
bzl_library(
name = "platforms",
srcs = ["platforms.bzl"],
)
bzl_library(
name = "providers",
srcs = ["providers.bzl"],
)
bzl_library(
name = "resolved_toolchain",
srcs = ["resolved_toolchain.bzl"],
)
bzl_library(
name = "show",
srcs = ["show.bzl"],
)
================================================
FILE: kustomize/private/extension.bzl
================================================
"""Module extension for kustomize toolchain."""
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("//kustomize/private:platforms.bzl", "PLATFORMS")
load("//kustomize/private/versions:versions.bzl", "LATEST_KUSTOMIZE_VERSION", "VERSIONS")
def _kustomize_hub_impl(rctx):
kustomize_hub_build_content = """
package(
default_visibility = ["//visibility:public"]
)
"""
toolchains_build_content = ""
for [platform, meta] in PLATFORMS.items():
toolchains_build_content += """
toolchain(
name = "{platform}_toolchain",
exec_compatible_with = {compatible_with},
toolchain = "@kustomize_{platform}//:kustomize_toolchain",
toolchain_type = "@rules_gitops//kustomize:toolchain_type",
)
""".format(
platform = platform,
compatible_with = meta.compatible_with,
)
rctx.file("toolchains/BUILD.bazel", content = toolchains_build_content)
rctx.file("BUILD.bazel", content = kustomize_hub_build_content)
_kustomize_hub = repository_rule(
implementation = _kustomize_hub_impl,
attrs = {},
)
KUSTOMIZE_TOOLCHAIN_BUILD = """load("@rules_gitops//kustomize:defs.bzl", "kustomize_toolchain")
kustomize_toolchain(
name = "kustomize_toolchain",
executable = "kustomize"
)
"""
def _kustomize_extension_impl(module_ctx):
kustomize_version = LATEST_KUSTOMIZE_VERSION
for mod in module_ctx.modules:
if len(mod.tags.toolchain) > 1:
fail("Expected kustomize toolchain to only be declared once")
# Allow the root module to override the kustomize toolchain version
if mod.is_root and len(mod.tags.toolchain) == 1:
kustomize_version = mod.tags.toolchain[0].version
if not VERSIONS[kustomize_version]:
fail("No matching version found for kustomize v{}".format(kustomize_version))
binaries = VERSIONS[kustomize_version]
for [platform, sha256] in binaries.items():
http_archive(
name = "kustomize_{}".format(platform),
urls = [
"https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v{version}/kustomize_v{version}_{platform}.tar.gz".format(version = kustomize_version, platform = platform.replace("-", "_")),
],
build_file_content = KUSTOMIZE_TOOLCHAIN_BUILD,
sha256 = sha256,
)
_kustomize_hub(
name = "kustomize",
)
return module_ctx.extension_metadata()
kustomize = module_extension(
implementation = _kustomize_extension_impl,
tag_classes = {
"toolchain": tag_class(
attrs = {
"version": attr.string(
doc = "kustomize binary version",
),
},
),
},
)
================================================
FILE: kustomize/private/kustomization.bzl
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""Rule for building kustomization overlays."""
load("//adapters:providers.bzl", "K8sPushInfo")
load("//kustomize/private:providers.bzl", "KustomizeInfo")
load("//stamper:stamp.bzl", "stamp")
def _stamp_file(ctx, infile, output):
stamps = [ctx.file._info_file]
stamp_args = [
"--stamp-info-file=%s" % sf.path
for sf in stamps
]
ctx.actions.run(
executable = ctx.executable._stamper,
arguments = [
"--format-file=%s" % infile.path,
"--output=%s" % output.path,
] + stamp_args,
inputs = [infile] + stamps,
outputs = [output],
mnemonic = "Stamp",
tools = [ctx.executable._stamper],
)
def _is_ignored_src(src):
basename = src.rsplit("/", 1)[-1]
return basename.startswith(".")
VERIFY_IMAGE_SCRIPT = """
if grep -E 'image:\\s+@{{0,2}}//' {out}; then
echo "ERROR: Found unreplaced bazel label in kustomize output" >&2
exit 1
fi
"""
_script_template = """\
#!/usr/bin/env bash
set -euo pipefail
{kustomize} build --load-restrictor LoadRestrictionsNone {kustomize_dir} {template_part} {resolver_part} >{out}
{verify}
"""
def _kustomization_impl(ctx):
kustomization_yaml_file = ctx.actions.declare_file(ctx.attr.name + "/kustomization.yaml")
root = kustomization_yaml_file.dirname
upupup = "/".join([".."] * (root.count("/") + 1))
use_stamp = False
tmpfiles = []
kustomization_yaml = "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"
kustomization_yaml += "sortOptions:\n order: legacy\n"
kustomization_yaml += "resources:\n"
for _, f in enumerate(ctx.files.manifests):
kustomization_yaml += "- {}/{}\n".format(upupup, f.path)
if ctx.attr.namespace:
kustomization_yaml += "namespace: '{}'\n".format(ctx.attr.namespace)
use_stamp = use_stamp or "{" in ctx.attr.namespace
if ctx.attr.name_prefix:
kustomization_yaml += "namePrefix: '{}'\n".format(ctx.attr.name_prefix)
use_stamp = use_stamp or "{" in ctx.attr.name_prefix
if ctx.attr.name_suffix:
kustomization_yaml += "nameSuffix: '{}'\n".format(ctx.attr.name_suffix)
use_stamp = use_stamp or "{" in ctx.attr.name_suffix
if ctx.attr.configurations:
kustomization_yaml += "configurations:\n"
for _, f in enumerate(ctx.files.configurations):
kustomization_yaml += "- {}/{}\n".format(upupup, f.path)
if ctx.files.patches:
kustomization_yaml += "patches:\n"
for _, f in enumerate(ctx.files.patches):
# TODO: this changed in later versions of kustomize
kustomization_yaml += "- path: {}/{}\n".format(upupup, f.path)
if ctx.attr.image_name_patches or ctx.attr.image_tag_patches:
kustomization_yaml += "images:\n"
for image, new_tag in ctx.attr.image_tag_patches.items():
new_name = ctx.attr.image_name_patches.get(image, default = None)
kustomization_yaml += "- name: \"{}\"\n".format(image)
kustomization_yaml += " newTag: \"{}\"\n".format(new_tag)
if new_name != None:
kustomization_yaml += " newName: \"{}\"\n".format(new_name)
for image, new_name in ctx.attr.image_name_patches.items():
if ctx.attr.image_tag_patches.get(image, default = None) == None:
kustomization_yaml += "- name: \"{}\"\n".format(image)
kustomization_yaml += " newName: \"{}\"\n".format(new_name)
if ctx.attr.common_labels:
kustomization_yaml += "labels:\n- includeSelectors: true\n pairs:\n"
for k in ctx.attr.common_labels:
use_stamp = use_stamp or "{" in ctx.attr.common_labels[k]
kustomization_yaml += " {}: '{}'\n".format(k, ctx.attr.common_labels[k])
if ctx.attr.common_annotations:
kustomization_yaml += "commonAnnotations:\n"
for k in ctx.attr.common_annotations:
use_stamp = use_stamp or "{" in ctx.attr.common_annotations[k]
kustomization_yaml += " {}: '{}'\n".format(k, ctx.attr.common_annotations[k])
kustomization_yaml += "generatorOptions:\n"
kustomization_yaml += " disableNameSuffixHash: {}\n".format(str(ctx.attr.disable_name_suffix_hash).lower())
if ctx.attr.configmaps_srcs:
maps = dict() # configmap name to list of File objects
for target in ctx.attr.configmaps_srcs:
for src in target.files.to_list():
# ignore dot files
if _is_ignored_src(src.path):
continue
mapname = src.path.rsplit("/")[-2]
if not mapname in maps:
maps[mapname] = []
maps[mapname].append(src)
kustomization_yaml += "configMapGenerator:\n"
for cmname in maps:
kustomization_yaml += "- name: {}\n".format(cmname)
kustomization_yaml += " files:\n"
for f in maps[cmname]:
kustomization_yaml += " - {}/{}\n".format(upupup, f.path)
if ctx.attr.secrets_srcs:
maps = dict() # secret name to list of File objects
for target in ctx.attr.secrets_srcs:
for src in target.files.to_list():
# ignore dot files
if _is_ignored_src(src.path):
continue
mapname = src.path.rsplit("/")[-2]
if not mapname in maps:
maps[mapname] = []
maps[mapname].append(src)
kustomization_yaml += "secretGenerator:\n"
for cmname in maps:
kustomization_yaml += "- name: {}\n".format(cmname)
kustomization_yaml += " type: Opaque\n"
kustomization_yaml += " files:\n"
for f in maps[cmname]:
kustomization_yaml += " - {}/{}\n".format(upupup, f.path)
if use_stamp:
kustomization_yaml_unstamped_file = ctx.actions.declare_file(ctx.attr.name + "/unstamped.yaml")
ctx.actions.write(kustomization_yaml_unstamped_file, kustomization_yaml)
_stamp_file(ctx, kustomization_yaml_unstamped_file, kustomization_yaml_file)
else:
ctx.actions.write(kustomization_yaml_file, kustomization_yaml)
transitive_runfiles = []
resolver_part = ""
if ctx.attr.images:
resolver_part += " | {resolver} ".format(resolver = ctx.executable._resolver.path)
tmpfiles.append(ctx.executable._resolver)
for img in ctx.attr.images:
kpi = img[K8sPushInfo]
regrepo = kpi.registry + "/" + kpi.repository
if "{" in regrepo:
regrepo = stamp(ctx, regrepo, tmpfiles, ctx.attr.name + regrepo.replace("/", "_"))
resolver_part += " --image {}={}@$(cat {})".format(str(kpi.image_label).replace("@@//", "//"), regrepo, kpi.digestfile.path)
tmpfiles.append(kpi.digestfile)
transitive_runfiles.append(img[DefaultInfo].default_runfiles)
template_part = ""
if ctx.attr.substitutions or ctx.attr.deps:
template_part += "| {} --stamp_info_file={} ".format(ctx.executable._template_engine.path, ctx.file._info_file.path)
tmpfiles.append(ctx.executable._template_engine)
tmpfiles.append(ctx.file._info_file)
for k in ctx.attr.substitutions:
template_part += "--variable=%s=%s " % (k, ctx.attr.substitutions[k])
if ctx.attr.start_tag:
template_part += "--start_tag=%s " % ctx.attr.start_tag
if ctx.attr.end_tag:
template_part += "--end_tag=%s " % ctx.attr.end_tag
d = {
str(ctx.attr.deps[i].label): ctx.files.deps[i].path
for i in range(0, len(ctx.attr.deps))
}
template_part += " ".join(["--imports=%s=%s" % (k, d[k]) for k in d])
template_part += " "
template_part += " ".join([
"--imports=%s=%s" % (k, d[str(ctx.label.relative(ctx.attr.deps_aliases[k]))])
for k in ctx.attr.deps_aliases
])
# Image name substitutions
if ctx.attr.images:
for img in ctx.attr.images:
kpi = img[K8sPushInfo]
regrepo = kpi.registry + "/" + kpi.repository
if "{" in regrepo:
regrepo = stamp(ctx, regrepo, tmpfiles, ctx.attr.name + regrepo.replace("/", "_"))
template_part += " --variable={}={}@$(cat {})".format(str(kpi.image_label).replace("@@//", "//"), regrepo, kpi.digestfile.path)
# Image digest
template_part += " --variable={}=$(cat {} | cut -d ':' -f 2)".format(str(kpi.image_label) + ".digest", kpi.digestfile.path)
template_part += " --variable={}=$(cat {} | cut -c 8-17)".format(str(kpi.image_label) + ".short-digest", kpi.digestfile.path)
if str(kpi.image_label).startswith("@//"):
# Bazel 6 add a @ prefix to the image label
label = str(kpi.image_label)[1:]
template_part += " --variable={}=$(cat {} | cut -d ':' -f 2)".format(str(label) + ".digest", kpi.digestfile.path)
template_part += " --variable={}=$(cat {} | cut -c 8-17)".format(str(label) + ".short-digest", kpi.digestfile.path)
if hasattr(kpi, "legacy_image_name"):
template_part += " --variable={}={}@$(cat {})".format(kpi.legacy_image_name, regrepo, kpi.digestfile.path)
template_part += " "
kustomize_executable = ctx.toolchains["@rules_gitops//kustomize:toolchain_type"].kustomizeinfo.executable
script = ctx.actions.declare_file("%s-kustomize" % ctx.label.name)
script_content = _script_template.format(
kustomize = kustomize_executable.path,
kustomize_dir = root,
resolver_part = resolver_part,
template_part = template_part,
out = ctx.outputs.yaml.path,
verify = VERIFY_IMAGE_SCRIPT.format(
out = ctx.outputs.yaml.path,
) if ctx.attr.verify_images else "",
)
ctx.actions.write(script, script_content, is_executable = True)
ctx.actions.run(
outputs = [ctx.outputs.yaml],
inputs = ctx.files.manifests + ctx.files.configmaps_srcs + ctx.files.secrets_srcs + ctx.files.configurations + [kustomization_yaml_file] + tmpfiles + ctx.files.patches + ctx.files.deps,
executable = script,
mnemonic = "Kustomize",
tools = [kustomize_executable],
)
runfiles = ctx.runfiles(files = ctx.files.deps).merge_all(transitive_runfiles)
transitive_files = [m[DefaultInfo].files for m in ctx.attr.manifests if KustomizeInfo in m]
transitive_files += [obj[DefaultInfo].files for obj in ctx.attr.objects]
transitive_image_pushes = [m[KustomizeInfo].image_pushes for m in ctx.attr.manifests if KustomizeInfo in m]
transitive_image_pushes += [obj[KustomizeInfo].image_pushes for obj in ctx.attr.objects]
return [
DefaultInfo(
files = depset(
[ctx.outputs.yaml],
transitive = transitive_files,
),
runfiles = runfiles,
),
KustomizeInfo(
image_pushes = depset(
ctx.attr.images,
transitive = transitive_image_pushes,
),
),
]
kustomization = rule(
implementation = _kustomization_impl,
attrs = {
"configmaps_srcs": attr.label_list(allow_files = True),
"secrets_srcs": attr.label_list(allow_files = True),
"deps_aliases": attr.string_dict(default = {}),
"disable_name_suffix_hash": attr.bool(default = True),
"end_tag": attr.string(default = "}}"),
"images": attr.label_list(doc = "a list of image pushes used in manifests", providers = [K8sPushInfo]),
"manifests": attr.label_list(allow_files = True),
"name_prefix": attr.string(),
"name_suffix": attr.string(),
"namespace": attr.string(),
"objects": attr.label_list(doc = "a list of dependent kustomize objects", providers = [KustomizeInfo]),
"patches": attr.label_list(allow_files = True),
"image_name_patches": attr.string_dict(default = {}, doc = "set new names for selected images"),
"image_tag_patches": attr.string_dict(default = {}, doc = "set new tags for selected images"),
"start_tag": attr.string(default = "{{"),
"substitutions": attr.string_dict(default = {}),
"deps": attr.label_list(default = [], allow_files = True),
"configurations": attr.label_list(allow_files = True),
"common_labels": attr.string_dict(default = {}),
"common_annotations": attr.string_dict(default = {}),
"verify_images": attr.bool(doc = "check whether all images which point to bazel labels were resolved", default = True),
"_build_user_value": attr.label(
default = Label("//stamper:build_user_value.txt"),
allow_single_file = True,
),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
"_resolver": attr.label(
default = Label("//resolver:resolver"),
cfg = "exec",
executable = True,
),
"_stamper": attr.label(
default = Label("//stamper:stamper"),
cfg = "exec",
executable = True,
allow_files = True,
),
"_template_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
},
outputs = {
"yaml": "%{name}.yaml",
},
toolchains = ["@rules_gitops//kustomize:toolchain_type"],
)
================================================
FILE: kustomize/private/kustomize_binary.bzl
================================================
"""
Simple rule for running kustomize from the toolchain config
"""
def _kustomize_binary(ctx):
executable = ctx.toolchains["@rules_gitops//kustomize:toolchain_type"].kustomizeinfo.executable
kustomize = ctx.actions.declare_file(ctx.label.name)
ctx.actions.symlink(
output = kustomize,
target_file = executable,
is_executable = True,
)
return [
DefaultInfo(
runfiles = ctx.runfiles(files = [executable, kustomize]),
executable = kustomize,
),
]
kustomize_binary = rule(
implementation = _kustomize_binary,
attrs = {},
toolchains = ["@rules_gitops//kustomize:toolchain_type"],
executable = True,
)
================================================
FILE: kustomize/private/platforms.bzl
================================================
"""Platform definitions for kustomize toolchain."""
PLATFORMS = {
"darwin_amd64": struct(
compatible_with = [
"@platforms//os:macos",
"@platforms//cpu:x86_64",
],
),
"darwin_arm64": struct(
compatible_with = [
"@platforms//os:macos",
"@platforms//cpu:arm64",
],
),
"linux_amd64": struct(
compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
),
"linux_arm64": struct(
compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
),
}
================================================
FILE: kustomize/private/providers.bzl
================================================
"""Provider definitions for kustomize rules."""
KustomizeToolchainInfo = provider(
doc = "Toolchain information about the kustomize executable",
fields = {
"target_tool_path": "Path to the kustomize executable for the target platform.",
"executable": "Hermetically download toolchain executable file",
},
)
KustomizeInfo = provider(
doc = "Information about a kustomization target",
fields = {
"image_pushes": "depset of image push executables for images referenced by this kustomization",
},
)
================================================
FILE: kustomize/private/resolved_toolchain.bzl
================================================
"""This module implements an alias rule to the resolved toolchain.
"""
DOC = """\
Exposes a concrete toolchain which is the result of Bazel resolving the
toolchain for the execution or target platform.
Workaround for https://github.com/bazelbuild/bazel/issues/14009
"""
# Forward all the providers
def _resolved_toolchain_impl(ctx):
toolchain_info = ctx.toolchains["//kustomize:toolchain_type"]
return [
toolchain_info,
toolchain_info.default,
toolchain_info.kustomizeinfo,
toolchain_info.template_variables,
]
# Copied from java_toolchain_alias
# https://cs.opensource.google/bazel/bazel/+/master:tools/jdk/java_toolchain_alias.bzl
resolved_toolchain = rule(
implementation = _resolved_toolchain_impl,
toolchains = ["//kustomize:toolchain_type"],
doc = DOC,
)
================================================
FILE: kustomize/private/show.bzl
================================================
"""Rule for displaying rendered kustomize output."""
def _show_impl(ctx):
script_content = "#!/usr/bin/env bash\nset -e\n"
kustomize_outputs = []
script_template = "{template_engine} --template={infile} --variable=NAMESPACE={namespace} --stamp_info_file={info_file}\n"
for dep in ctx.attr.src.files.to_list():
kustomize_outputs.append(script_template.format(
infile = dep.short_path,
template_engine = ctx.executable._template_engine.short_path,
namespace = ctx.attr.namespace,
info_file = ctx.file._info_file.short_path,
))
# ensure kustomize outputs are separated by '---' delimiters
script_content += "echo '---'\n".join(kustomize_outputs)
ctx.actions.write(ctx.outputs.executable, script_content, is_executable = True)
return [
DefaultInfo(runfiles = ctx.runfiles(files = [ctx.executable._template_engine, ctx.file._info_file] + ctx.files.src)),
]
show = rule(
implementation = _show_impl,
attrs = {
"src": attr.label(
doc = "Input file.",
mandatory = True,
),
"namespace": attr.string(
doc = "kubernetes namespace.",
mandatory = True,
),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
"_template_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
},
executable = True,
)
================================================
FILE: kustomize/private/tests/BUILD.bazel
================================================
load("//kustomize/private:kustomization.bzl", "kustomization")
load("//tools:util.bzl", "golden_test")
# Verify that rule is combining files without processing
kustomization(
name = "raw",
testonly = True,
images = [
"//tests/images:k8s_image",
],
manifests = [
"deployment.yaml",
"service.yaml",
"crb.yaml",
],
namespace = "",
verify_images = False,
)
golden_test(
name = "raw_test",
in_file = "raw",
)
================================================
FILE: kustomize/private/tests/crb.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crb-name
subjects:
- kind: Group
name: crb-subject
roleRef:
kind: ClusterRole
name: cluster-admin
================================================
FILE: kustomize/private/tests/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: //tests/images:nonexistent
================================================
FILE: kustomize/private/tests/goldens/raw.golden
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crb-name
roleRef:
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: crb-subject
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
ports:
- name: web
port: 80
targetPort: 8080
selector:
app: myapp
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- image: //tests/images:nonexistent
name: myapp
================================================
FILE: kustomize/private/tests/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
ports:
- port: 80
name: web
targetPort: 8080
selector:
app: myapp
================================================
FILE: kustomize/private/toolchain.bzl
================================================
"Kustomize toolchain rule"
load("//kustomize/private:providers.bzl", "KustomizeToolchainInfo")
# Avoid using non-normalized paths (workspace/../other_workspace/path)
def _to_manifest_path(ctx, file):
if file.short_path.startswith("../"):
return "external/" + file.short_path[3:]
else:
return ctx.workspace_name + "/" + file.short_path
def _kustomize_toolchain_impl(ctx):
executable = ctx.file.executable
target_tool_path = _to_manifest_path(ctx, executable)
template_variables = platform_common.TemplateVariableInfo({
"KUSTOMIZE_BIN": target_tool_path,
})
default = DefaultInfo(
files = depset([executable]),
runfiles = ctx.runfiles(files = [executable]),
)
kustomizeinfo = KustomizeToolchainInfo(
target_tool_path = target_tool_path,
executable = executable,
)
toolchain_info = platform_common.ToolchainInfo(
kustomizeinfo = kustomizeinfo,
template_variables = template_variables,
default = default,
)
return [
default,
toolchain_info,
template_variables,
]
kustomize_toolchain = rule(
implementation = _kustomize_toolchain_impl,
attrs = {
"executable": attr.label(
doc = "A hermetically downloaded executable target for the target platform.",
mandatory = True,
allow_single_file = True,
),
},
)
================================================
FILE: kustomize/private/versions/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["update_versions.go"],
importpath = "github.com/adobe/rules_gitops/kustomize/private/versions",
visibility = ["//visibility:private"],
)
go_binary(
name = "update_versions",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
bzl_library(
name = "versions",
srcs = ["versions.bzl"],
visibility = ["//kustomize:__subpackages__"],
)
================================================
FILE: kustomize/private/versions/update_versions.go
================================================
// update_versions downloads kustomize release information from GitHub
// and generates a versions.bzl file with SHA256 digests for each platform.
//
// Usage:
//
// go run update_versions.go
//
// The script expects BUILD_WORKSPACE_DIRECTORY to be set, or writes to
// the default location relative to this file's directory.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
const (
releasesURL = "https://api.github.com/repos/kubernetes-sigs/kustomize/releases"
outputFile = "kustomize/private/versions/versions.bzl"
)
// Asset represents a GitHub release asset
type Asset struct {
Name string `json:"name"`
Digest string `json:"digest"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// Release represents a GitHub release
type Release struct {
TagName string `json:"tag_name"`
PublishedAt string `json:"published_at"`
Assets []Asset `json:"assets"`
}
// VersionInfo holds platform -> sha256 mappings for a version
type VersionInfo struct {
Platforms map[string]string
}
func main() {
log.SetFlags(0)
// Fetch releases from GitHub
releases, err := fetchReleases()
if err != nil {
log.Fatalf("Failed to fetch releases: %v", err)
}
// Parse releases and extract version info
versions, latestVersion := parseReleases(releases)
// Generate versions.bzl content
content := generateBzl(versions, latestVersion)
// Determine output path
outputPath := getOutputPath()
// Write the file
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
log.Fatalf("Failed to create directory: %v", err)
}
if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil {
log.Fatalf("Failed to write file: %v", err)
}
log.Printf("Successfully wrote %s with %d versions", outputPath, len(versions))
}
func fetchReleases() ([]Release, error) {
// GitHub API may paginate, so we need to fetch all pages
var allReleases []Release
url := releasesURL + "?per_page=100"
for url != "" {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "rules_gitops-version-updater")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
}
var releases []Release
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return nil, fmt.Errorf("failed to decode JSON: %w", err)
}
allReleases = append(allReleases, releases...)
// Check for pagination
url = getNextPageURL(resp.Header.Get("Link"))
}
return allReleases, nil
}
func getNextPageURL(linkHeader string) string {
if linkHeader == "" {
return ""
}
// Parse Link header: ; rel="next", ; rel="last"
for _, part := range strings.Split(linkHeader, ",") {
part = strings.TrimSpace(part)
if strings.Contains(part, `rel="next"`) {
// Extract URL between < and >
start := strings.Index(part, "<")
end := strings.Index(part, ">")
if start >= 0 && end > start {
return part[start+1 : end]
}
}
}
return ""
}
func parseReleases(releases []Release) (map[string]*VersionInfo, string) {
versions := make(map[string]*VersionInfo)
// Regex to match kustomize version tags
versionRe := regexp.MustCompile(`^kustomize/v(\d+\.\d+\.\d+)$`)
// Regex to extract platform from asset name
assetRe := regexp.MustCompile(`^kustomize_v[\d.]+_([a-z]+_[a-z0-9]+)\.tar\.gz$`)
// Track latest version by publish date
latestVersion := ""
latestPublishedAt := ""
for _, release := range releases {
matches := versionRe.FindStringSubmatch(release.TagName)
if matches == nil {
continue // Not a kustomize release
}
version := matches[1]
versionInfo := &VersionInfo{
Platforms: make(map[string]string),
}
for _, asset := range release.Assets {
assetMatches := assetRe.FindStringSubmatch(asset.Name)
if assetMatches == nil {
continue
}
platform := assetMatches[1]
// Extract SHA256 from digest (format: "sha256:...")
sha256 := strings.TrimPrefix(asset.Digest, "sha256:")
if sha256 == "" {
continue
}
versionInfo.Platforms[platform] = sha256
}
if len(versionInfo.Platforms) > 0 {
versions[version] = versionInfo
// Track latest by publish date (ISO 8601 format is lexicographically sortable)
if release.PublishedAt > latestPublishedAt {
latestPublishedAt = release.PublishedAt
latestVersion = version
}
}
}
return versions, latestVersion
}
func generateBzl(versions map[string]*VersionInfo, latestVersion string) string {
var sb strings.Builder
// Sort versions for consistent output (reverse alphabetical puts newer versions first)
versionList := make([]string, 0, len(versions))
for v := range versions {
versionList = append(versionList, v)
}
sort.Sort(sort.Reverse(sort.StringSlice(versionList)))
sb.WriteString(`"""Generated by update_versions.go - do not edit manually."""
`)
sb.WriteString(fmt.Sprintf("LATEST_KUSTOMIZE_VERSION = %q\n\n", latestVersion))
sb.WriteString("VERSIONS = {\n")
for _, version := range versionList {
info := versions[version]
sb.WriteString(fmt.Sprintf(" %q: {\n", version))
// Sort platforms for consistent output
platforms := make([]string, 0, len(info.Platforms))
for p := range info.Platforms {
platforms = append(platforms, p)
}
sort.Strings(platforms)
for _, platform := range platforms {
sha256 := info.Platforms[platform]
sb.WriteString(fmt.Sprintf(" %q: %q,\n", platform, sha256))
}
sb.WriteString(" },\n")
}
sb.WriteString("}\n")
return sb.String()
}
func getOutputPath() string {
// Check for BUILD_WORKSPACE_DIRECTORY environment variable
if wsDir := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); wsDir != "" {
return filepath.Join(wsDir, outputFile)
}
// Fallback: find workspace root by looking for MODULE.bazel or WORKSPACE
// Start from executable directory and walk up
exePath, err := os.Executable()
if err != nil {
log.Fatalf("Failed to get executable path: %v", err)
}
dir := filepath.Dir(exePath)
// Also try current working directory
cwd, _ := os.Getwd()
for _, startDir := range []string{dir, cwd} {
d := startDir
for d != "/" && d != "." {
if _, err := os.Stat(filepath.Join(d, "MODULE.bazel")); err == nil {
return filepath.Join(d, outputFile)
}
if _, err := os.Stat(filepath.Join(d, "WORKSPACE")); err == nil {
return filepath.Join(d, outputFile)
}
d = filepath.Dir(d)
}
}
log.Fatalf("Could not find workspace root. Set BUILD_WORKSPACE_DIRECTORY or run from within the workspace.")
return ""
}
================================================
FILE: kustomize/private/versions/versions.bzl
================================================
"""Generated by update_versions.go - do not edit manually."""
LATEST_KUSTOMIZE_VERSION = "5.8.0"
VERSIONS = {
"5.8.0": {
"darwin_amd64": "2deaa6f96450c0b3204cccd9f159a22278eb6cf85ad545d212d608d2428aeb57",
"darwin_arm64": "d098f62ecda500c752303163838af823f947d245927f3000b629199c1eeeae0f",
"linux_amd64": "4dfa8307358dd9284aa4d2b1d5596766a65b93433e8fa3f9f74498941f01c5ef",
"linux_arm64": "a4f48b4c3d4ca97d748943e19169de85a2e86e80bcc09558603e2aa66fb15ce1",
"linux_ppc64le": "4352a29912b6a15688598ec192e79b841ee8b128ba411592d1de68b585009ec7",
"linux_s390x": "f7be07697c1ba872ff4e99806ac9a86faa7029e108a24ccfa7b47c04c1cc9c0d",
},
"5.7.1": {
"darwin_amd64": "4a0dff80c5644df6bc8f51b342842969004cb6ba5f94dddaabbea7483493273d",
"darwin_arm64": "073e9d16d5a235e2ff83e62d6b76edb5d962adbc33be1e4860c4b3f1f39b33b9",
"linux_amd64": "ea375e7372f9aa029129d4b2d16c66b7750b7f1213c4f66f910d981c895818d8",
"linux_arm64": "4261a040217df3bd6896597c3986d1465925726e4f22a945304b5233a4dcdbda",
"linux_ppc64le": "56b6fbf549080b14ddc738f10a05f78cbc5511cd7ab2014d9eb85f52bb4b7263",
"linux_s390x": "b1eee427af74f3bb53d96e3ba94d5cc7484a35bb1af1495488bc684d60df4488",
},
"5.7.0": {
"darwin_amd64": "277a7401f969ce3945e8f0ff8b0cce6f4353854db1ff89ba070001e3246e7f22",
"darwin_arm64": "c0dac68dc7870e1f673ae4d8fb554df971e0b9b9f0affc4be4c0852f62d0796e",
"linux_amd64": "0d98f06d6d2c2c0ff8923cc136a517af74aaa187f1b9f3e17ff370d0625ede84",
"linux_arm64": "744bb1bc1854b6634dea9eaf6db2f401a734ed25d6837baa6f91157d79c27d5e",
"linux_ppc64le": "752e750d5f349156ea228ae01cf57be22e6cc29f0f05748a1bca7fa870393561",
"linux_s390x": "64898beb154a111c1a98f8cff066fdfa866c4c73505e9a9b5fa6ec39f0292558",
},
}
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
":dependencyDashboard",
":enablePreCommit",
":semanticPrefixFixDepsChoreOthers",
"group:monorepos",
"group:recommended",
"replacements:all",
"workarounds:all"
],
"packageRules": [
{
"matchFiles": ["MODULE.bazel"],
"enabled": false
}
]
}
================================================
FILE: resolver/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["resolver.go"],
importpath = "github.com/adobe/rules_gitops/resolver",
visibility = ["//visibility:private"],
deps = ["//resolver/pkg:go_default_library"],
)
go_binary(
name = "resolver",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
================================================
FILE: resolver/pkg/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["resolver.go"],
importpath = "github.com/adobe/rules_gitops/resolver/pkg",
visibility = ["//visibility:public"],
deps = [
"@com_github_ghodss_yaml//:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
"@io_k8s_apimachinery//pkg/util/yaml:go_default_library",
],
)
# TODO(KZ): this test uses private manifests
go_test(
name = "go_default_test",
srcs = ["resolver_test.go"],
data = glob(["testdata/**"]),
deps = [":go_default_library"],
)
================================================
FILE: resolver/pkg/resolver.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package resolver
import (
"fmt"
"io"
"strings"
yamlenc "github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
)
// ResolveImages reads yaml or json stream from in, deserialize it and replace images with ones specified in imagemap
// and serialize it back into out stream.
func ResolveImages(in io.Reader, out io.Writer, imgmap map[string]string) error {
pt := imageTagTransformer{images: imgmap}
decoder := yaml.NewYAMLOrJSONDecoder(in, 1024)
var err error
firstObj := true
for err == nil || isEmptyYamlError(err) {
var obj unstructured.Unstructured
err = decoder.Decode(&obj)
if err != nil {
continue
}
if obj.GetName() == "" {
return fmt.Errorf("Missing metadata.name in object %v", obj)
}
if obj.GetKind() == "" {
return fmt.Errorf("Missing kind in object %v", obj)
}
pt.findAndReplaceTag(obj.Object)
buf, err := yamlenc.Marshal(obj.Object)
if err != nil {
return fmt.Errorf("Unable to marshal object %v", obj)
}
if firstObj {
firstObj = false
} else {
_, err = out.Write([]byte("---\n"))
if err != nil {
return err
}
}
_, err = out.Write(buf)
if err != nil {
return err
}
}
if err != io.EOF {
return err
}
return nil
}
func isEmptyYamlError(err error) bool {
return strings.Contains(err.Error(), "is missing in 'null'")
}
type imageTagTransformer struct {
images map[string]string
}
/*
findAndReplaceTag replaces the image tags inside one object
It searches the object for container session
then loops though all images inside containers session, finds matched ones and update the tag name
*/
func (pt *imageTagTransformer) findAndReplaceTag(obj map[string]interface{}) error {
found := false
// Update [container|spec].image
paths := []string{"container", "spec"}
for _, path := range paths {
_, found = obj[path]
if found {
err := pt.updateContainer(obj, path)
if err != nil {
return err
}
}
}
// Update containers.[image, image]
paths = []string{"containers", "initContainers"}
for _, path := range paths {
_, found = obj[path]
if found {
err := pt.updateContainers(obj, path)
if err != nil {
return err
}
}
}
if !found {
return pt.findContainers(obj)
}
return nil
}
func (pt *imageTagTransformer) updateContainers(obj map[string]interface{}, path string) error {
if obj[path] == nil {
return nil
}
switch containers := obj[path].(type) {
case []interface{}:
for i := range containers {
container, ok := containers[i].(map[string]interface{})
if !ok {
continue
}
image, found := container["image"]
if !found {
continue
}
imagename, imagenameOk := image.(string)
if imagenameOk {
if newname, ok := pt.images[imagename]; ok {
container["image"] = newname
continue
}
if strings.HasPrefix(imagename, "//") {
return fmt.Errorf("Unresolved image found: %s", imagename)
}
}
}
default:
return nil
}
return nil
}
func (pt *imageTagTransformer) updateContainer(obj map[string]interface{}, path string) error {
if obj[path] == nil {
return nil
}
container := obj[path].(map[string]interface{})
image, found := container["image"]
if found {
imagename, imagenameOk := image.(string)
if imagenameOk {
if strings.HasPrefix(imagename, "//") {
return fmt.Errorf("unresolved image found: %s", imagename)
}
if newname, ok := pt.images[imagename]; ok {
container["image"] = newname
}
}
}
return nil
}
func (pt *imageTagTransformer) findContainers(obj map[string]interface{}) error {
for key := range obj {
switch typedV := obj[key].(type) {
case map[string]interface{}:
err := pt.findAndReplaceTag(typedV)
if err != nil {
return err
}
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if ok {
err := pt.findAndReplaceTag(typedItem)
if err != nil {
return err
}
}
}
}
}
return nil
}
================================================
FILE: resolver/pkg/resolver_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package resolver_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
resolver "github.com/adobe/rules_gitops/resolver/pkg"
)
func TestNoError(t *testing.T) {
testcases := []struct {
name string
imgmap map[string]string
}{
{"happypath", map[string]string{
"salist": "docker.io/rtb/sacli/cmd/salist/image@sha256:5711bcf54511ab2fef6e08d9c9f9ae3f3a269e66834048465cc7502adb0d489b",
"filewatcher": "docker.io/kube/filewatcher/image:tag",
}},
{"cwf", map[string]string{
"helloworld-image": "docker.io/kube/hello/image:tag",
}},
{"flinkapp", map[string]string{
"flinkapp-image": "docker.io/kube/flink/image:tag",
}},
{"zk", map[string]string{
"zk-image": "dummy",
}},
{"emptyinit", map[string]string{
"helloworld-image": "docker.io/kube/hello/image:tag",
}},
{"digest", map[string]string{
"helloworld-image": "dummy",
}},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
infn := fmt.Sprintf("testdata/%s.yaml", testcase.name)
expectedfn := fmt.Sprintf("testdata/%s.expected.yaml", testcase.name)
inf, err := os.Open(infn)
if err != nil {
t.Errorf("Unable to open file %s", infn)
return
}
defer inf.Close()
expectedb, err := ioutil.ReadFile(expectedfn)
if err != nil {
t.Errorf("Unable to read file %s", expectedfn)
return
}
expected := strings.TrimSpace(string(expectedb))
var outbuf bytes.Buffer
err = resolver.ResolveImages(inf, &outbuf, testcase.imgmap)
if err != nil {
t.Errorf("Unexpected error %v", err)
return
}
if strings.TrimSpace(outbuf.String()) != expected {
t.Errorf("Unexpected output: %s", outbuf.String())
}
})
}
}
================================================
FILE: resolver/pkg/testdata/cwf.expected.yaml
================================================
apiVersion: apps/v1
kind: CronWorkFlow
metadata:
name: aaa
namespace: stats-dev
spec:
workflowSpec:
metadata:
labels:
app: app
templates:
container:
image: docker.io/kube/hello/image:tag
================================================
FILE: resolver/pkg/testdata/cwf.yaml
================================================
apiVersion: apps/v1
kind: CronWorkFlow
metadata:
name: aaa
namespace: stats-dev
spec:
workflowSpec:
metadata:
labels:
app: app
templates:
container:
image: helloworld-image
================================================
FILE: resolver/pkg/testdata/digest.expected.yaml
================================================
deployment-templates:
deployment:
containers:
hello:
image:
digest: sha256:{{@@//hello-world:image.digest}}
kind: CustomDeployment
metadata:
name: hellow-world
namespace: helm
================================================
FILE: resolver/pkg/testdata/digest.yaml
================================================
deployment-templates:
deployment:
containers:
hello:
image:
digest: "sha256:{{@@//hello-world:image.digest}}"
kind: CustomDeployment
metadata:
name: hellow-world
namespace: helm
================================================
FILE: resolver/pkg/testdata/emptyinit.expected.yaml
================================================
apiVersion: apps/v1
kind: CronWorkFlow
metadata:
name: aaa
namespace: stats-dev
spec:
workflowSpec:
metadata:
labels:
app: app
templates:
container:
image: docker.io/kube/hello/image:tag
initContainers: null
================================================
FILE: resolver/pkg/testdata/emptyinit.yaml
================================================
apiVersion: apps/v1
kind: CronWorkFlow
metadata:
name: aaa
namespace: stats-dev
spec:
workflowSpec:
metadata:
labels:
app: app
templates:
initContainers:
container:
image: helloworld-image
================================================
FILE: resolver/pkg/testdata/flinkapp.expected.yaml
================================================
apiVersion: flink.k8s.io/v1beta1
kind: FlinkApplication
metadata:
name: wordcount-operator-example
spec:
image: docker.io/kube/flink/image:tag
================================================
FILE: resolver/pkg/testdata/flinkapp.yaml
================================================
apiVersion: flink.k8s.io/v1beta1
kind: FlinkApplication
metadata:
name: wordcount-operator-example
spec:
image: flinkapp-image
================================================
FILE: resolver/pkg/testdata/happypath.expected.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: prometheus2
name: prometheus2
namespace: rtb-prod
spec:
replicas: 1
selector:
matchLabels:
app: prometheus2
strategy:
type: Recreate
template:
metadata:
labels:
app: prometheus2
name: prometheus2
spec:
containers:
- args:
- -logtostderr
- -zk=rtb-zk01.us-east-1a.public:2181,rtb-zk02.us-east-1a.public:2181,rtb-zk03.us-east-1a.public:2181,rtb-zk04.us-east-1a.public:2181,rtb-zk05.us-east-1a.public:2181
- -ofile=/etc/prometheus-sa/sa-rtb.yaml
- /sa/presence/rtb
image: docker.io/rtb/sacli/cmd/salist/image@sha256:5711bcf54511ab2fef6e08d9c9f9ae3f3a269e66834048465cc7502adb0d489b
name: samonitor
volumeMounts:
- mountPath: /etc/prometheus-sa
name: shared-data
- args:
- --storage.tsdb.path=/data/
- --storage.tsdb.retention=360h
- --web.enable-lifecycle
- --web.console.libraries=/etc/prometheus/console_libraries
- --web.console.templates=/etc/prometheus/consoles
- --config.file=/etc/prometheus-configs/prometheus.config
- --web.external-url=https://prometheus.rtb-prod.us-east-1.k8s.tubemogul.info/
image: prom/prometheus:v2.2.1
livenessProbe:
httpGet:
path: /-/healthy
port: 9090
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
name: prometheus
ports:
- containerPort: 9090
name: webui
readinessProbe:
httpGet:
path: /-/ready
port: 9090
scheme: HTTP
resources:
limits:
cpu: "10"
memory: 22Gi
requests:
cpu: "10"
memory: 22Gi
volumeMounts:
- mountPath: /etc/prometheus-sa
name: shared-data
- mountPath: /etc/prometheus-configs
name: config-volume
- mountPath: /data
name: data-volume
- args:
- -file=/etc/prometheus-configs/..data
- -notify-http-method=POST
- -notify-http-url=http://127.0.0.1:9090/-/reload
image: docker.io/kube/filewatcher/image:tag
name: filewatcher
resources:
limits:
cpu: 0.01
memory: 10Mi
requests:
cpu: 0.01
memory: 10Mi
volumeMounts:
- mountPath: /etc/prometheus-configs
name: config-volume
securityContext:
fsGroup: 99
runAsUser: 99
volumes:
- emptyDir: {}
name: shared-data
- configMap:
name: prometheus2
name: config-volume
- name: data-volume
persistentVolumeClaim:
claimName: prometheus2-data
================================================
FILE: resolver/pkg/testdata/happypath.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: prometheus2
name: prometheus2
namespace: rtb-prod
spec:
replicas: 1
selector:
matchLabels:
app: prometheus2
strategy:
type: Recreate
template:
metadata:
labels:
app: prometheus2
name: prometheus2
spec:
containers:
- args:
- -logtostderr
- -zk=rtb-zk01.us-east-1a.public:2181,rtb-zk02.us-east-1a.public:2181,rtb-zk03.us-east-1a.public:2181,rtb-zk04.us-east-1a.public:2181,rtb-zk05.us-east-1a.public:2181
- -ofile=/etc/prometheus-sa/sa-rtb.yaml
- /sa/presence/rtb
image: salist
name: samonitor
volumeMounts:
- mountPath: /etc/prometheus-sa
name: shared-data
- args:
- --storage.tsdb.path=/data/
- --storage.tsdb.retention=360h
- --web.enable-lifecycle
- --web.console.libraries=/etc/prometheus/console_libraries
- --web.console.templates=/etc/prometheus/consoles
- --config.file=/etc/prometheus-configs/prometheus.config
- --web.external-url=https://prometheus.rtb-prod.us-east-1.k8s.tubemogul.info/
image: prom/prometheus:v2.2.1
livenessProbe:
httpGet:
path: /-/healthy
port: 9090
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
name: prometheus
ports:
- containerPort: 9090
name: webui
readinessProbe:
httpGet:
path: /-/ready
port: 9090
scheme: HTTP
resources:
limits:
cpu: "10"
memory: 22Gi
requests:
cpu: "10"
memory: 22Gi
volumeMounts:
- mountPath: /etc/prometheus-sa
name: shared-data
- mountPath: /etc/prometheus-configs
name: config-volume
- mountPath: /data
name: data-volume
- args:
- -file=/etc/prometheus-configs/..data
- -notify-http-method=POST
- -notify-http-url=http://127.0.0.1:9090/-/reload
image: filewatcher
name: filewatcher
resources:
limits:
cpu: 0.01
memory: 10Mi
requests:
cpu: 0.01
memory: 10Mi
volumeMounts:
- mountPath: /etc/prometheus-configs
name: config-volume
securityContext:
fsGroup: 99
runAsUser: 99
volumes:
- emptyDir: {}
name: shared-data
- configMap:
name: prometheus2
name: config-volume
- name: data-volume
persistentVolumeClaim:
claimName: prometheus2-data
================================================
FILE: resolver/pkg/testdata/zk.expected.yaml
================================================
apiVersion: zookeeper.pravega.io/v1beta1
kind: ZookeeperCluster
metadata:
name: zk-cluster
spec:
image:
repository: repository
tag: 1
================================================
FILE: resolver/pkg/testdata/zk.yaml
================================================
apiVersion: zookeeper.pravega.io/v1beta1
kind: ZookeeperCluster
metadata:
name: zk-cluster
spec:
image:
repository: repository
tag: 1
================================================
FILE: resolver/resolver.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"strings"
resolver "github.com/adobe/rules_gitops/resolver/pkg"
)
type imagesFlags map[string]string
func (i *imagesFlags) String() string {
return fmt.Sprintf("%v", *i)
}
func (i *imagesFlags) Set(value string) error {
v := strings.SplitN(value, "=", 2)
if len(v) != 2 {
return errors.New("image parameter should be in form imagename=imagevalue")
}
(*i)[strings.TrimSpace(v[0])] = strings.TrimSpace(v[1])
return nil
}
var (
inf = flag.String("infile", "", "Input file")
outf = flag.String("outfile", "", "Out file")
images = make(imagesFlags)
)
func main() {
flag.Var(&images, "image", "imagename=imagevalue")
flag.Parse()
infile := os.Stdin
if *inf != "" {
f, err := os.Open(*inf)
if err != nil {
log.Fatalf("Unable to open file %s for reading: %s", *inf, err)
}
defer f.Close()
infile = f
}
outfile := os.Stdout
if *outf != "" {
f, err := os.Create(*outf)
if err != nil {
log.Fatalf("Unable to create file %s for reading: %s", *outf, err)
}
defer f.Close()
outfile = f
}
err := resolver.ResolveImages(infile, outfile, images)
if err != nil {
log.Fatalf("Unable to process: %s", err)
}
}
================================================
FILE: stamper/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_binary", "go_library")
load(":stamp.bzl", "more_stable_status", "stamp_value")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/adobe/rules_gitops/stamper",
visibility = ["//visibility:private"],
deps = ["//templating/fasttemplate:go_default_library"],
)
go_binary(
name = "stamper",
embed = [":go_default_library"],
visibility = ["//:__subpackages__"],
)
stamp_value(
name = "build_user_value",
str = "{BUILD_USER}",
visibility = ["//:__subpackages__"],
)
more_stable_status(
name = "more_stable_status",
vars = [
"BUILD_USER",
],
visibility = ["//:__subpackages__"],
)
bzl_library(
name = "stamp",
srcs = ["stamp.bzl"],
visibility = ["//visibility:public"],
)
================================================
FILE: stamper/main.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"flag"
"io/ioutil"
"log"
"os"
"strings"
"github.com/adobe/rules_gitops/templating/fasttemplate"
)
type arrayFlags []string
func (i *arrayFlags) String() string {
return ""
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
var (
stampInfoFile arrayFlags
output string
format, formatFile string
)
func init() {
flag.Var(&stampInfoFile, "stamp-info-file", "Paths to info_file and version_file files for stamping.")
flag.StringVar(&output, "output", "", "The output file")
flag.StringVar(&formatFile, "format-file", "", "The file containing stamp variables placeholders")
flag.StringVar(&format, "format", "", "The format string containing stamp variables")
}
func workspaceStatusDict(filenames []string) map[string]interface{} {
d := map[string]interface{}{}
for _, f := range filenames {
content, err := ioutil.ReadFile(f)
if err != nil {
log.Fatalf("Unable to read %s: %v", f, err)
}
for _, l := range strings.Split(string(content), "\n") {
sv := strings.SplitN(l, " ", 2)
if len(sv) == 2 {
d[sv[0]] = sv[1]
}
}
}
return d
}
func main() {
var err error
flag.Parse()
stamps := workspaceStatusDict(stampInfoFile)
if formatFile != "" {
if format != "" {
log.Fatal("only one of --format or --format-file should be used")
}
imp, err := ioutil.ReadFile(formatFile)
if err != nil {
log.Fatalf("Unable to read file %s: %v", formatFile, err)
}
format = string(imp)
}
outf := os.Stdout
if output != "" {
outf, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Fatalf("Unable to create output file %s: %v", output, err)
}
defer outf.Close()
}
_, err = fasttemplate.Execute(format, "{", "}", outf, stamps)
if err != nil {
log.Fatalf("Unable to execute template %s: %v", format, err)
}
}
================================================
FILE: stamper/stamp.bzl
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
"""Stamping utilities for replacing placeholders in strings."""
def stamp(ctx, string, _tmpfiles, tmpfilename):
"""Stamp provided string replacing placeholders like {BUILD_USER}.
Uses an optimization shortcut for BUILD_USER
Args:
ctx: The rule context.
string: The string containing placeholders to stamp.
_tmpfiles: Unused, kept for API compatibility.
tmpfilename: Base name for temporary files.
Returns:
a string suitable for inclusion into bash script.
"""
deps = []
if "{BUILD_USER}" in string and "{" not in string.format(BUILD_USER = ""):
# shortcut for only {BUILD_USER} in placeholders
string = string.format(
BUILD_USER = "$(cat %s)" % ctx.file._build_user_value.path,
)
deps.append(ctx.files._build_user_value[0])
return string, deps
stamps = [ctx.file._info_file]
stamp_args = [
"--stamp-info-file=%s" % sf.path
for sf in stamps
]
tmp_out_file = ctx.actions.declare_file(tmpfilename)
ctx.actions.run(
executable = ctx.executable._stamper,
arguments = [
"--format=%s" % string,
"--output=%s" % tmp_out_file.path,
] + stamp_args,
inputs = stamps,
outputs = [tmp_out_file],
mnemonic = "Stamp",
tools = [ctx.executable._stamper],
)
string = "$(cat {})".format(tmp_out_file.path)
deps.append(tmp_out_file)
return string, deps
def _stamp_value_impl(ctx):
stamps = [ctx.file._info_file]
stamp_args = [
"--stamp-info-file=%s" % sf.path
for sf in stamps
]
ctx.actions.run(
executable = ctx.executable._stamper,
arguments = [
"--format=%s" % ctx.attr.str,
"--output=%s" % ctx.outputs.out.path,
] + stamp_args,
inputs = stamps,
outputs = [ctx.outputs.out],
mnemonic = "Stamp",
tools = [ctx.executable._stamper],
)
stamp_value = rule(
implementation = _stamp_value_impl,
attrs = {
"str": attr.string(default = "{BUILD_USER}"),
"_info_file": attr.label(
default = Label("//stamper:more_stable_status.txt"),
allow_single_file = True,
),
"_stamper": attr.label(
default = Label("//stamper:stamper"),
cfg = "exec",
executable = True,
allow_files = True,
),
},
outputs = {
"out": "%{name}.txt",
},
)
def _more_stable_status_impl(ctx):
v = " ".join(["-e ^" + var for var in ctx.attr.vars])
ctx.actions.run_shell(
inputs = [ctx.info_file],
outputs = [ctx.outputs.out],
progress_message = "Filtering stable status file",
command = "grep {} {} >{}".format(v, ctx.info_file.path, ctx.outputs.out.path),
)
# Generate reduced more stable version of stable-status.txt
# Limited number of rows is extracted now to make it cacheable for CI/CD
more_stable_status = rule(
attrs = {
"vars": attr.string_list(
mandatory = True,
doc = "Variables to extract from stable_status.txt",
),
},
outputs = {
"out": "%{name}.txt",
},
implementation = _more_stable_status_impl,
)
================================================
FILE: templating/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_binary", "go_library")
licenses(["notice"]) # Apache 2.0
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/adobe/rules_gitops/templating",
visibility = ["//visibility:private"],
deps = ["//templating/fasttemplate:go_default_library"],
)
go_binary(
name = "fast_template_engine",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
================================================
FILE: templating/fasttemplate/BUILD.bazel
================================================
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["template.go"],
importpath = "github.com/adobe/rules_gitops/templating/fasttemplate",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"example_test.go",
"template_test.go",
],
embed = [":go_default_library"],
)
================================================
FILE: templating/fasttemplate/LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Aliaksandr Valialkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: templating/fasttemplate/README.md
================================================
fasttemplate
============
Simple and fast template engine for Go.
Forked from [fasttemplate](https://github.com/valyala/fasttemplate).
This package was modified from the original one:
1. usage of unsafe is removed
2. usage of buffer pools is removed
*Please note that fasttemplate doesn't do any escaping on template values
unlike [html/template](http://golang.org/pkg/html/template/) do. So values
must be properly escaped before passing them to fasttemplate.*
================================================
FILE: templating/fasttemplate/example_test.go
================================================
package fasttemplate
import (
"fmt"
"io"
"net/url"
)
func ExampleTemplate() {
template := "http://{{host}}/?foo={{bar}}{{bar}}&q={{query}}&baz={{baz}}"
// Substitution map.
// Since "baz" tag is missing in the map, it will be left unchanged.
m := map[string]interface{}{
"host": "google.com", // string - convenient
"bar": []byte("foobar"), // byte slice - the fastest
// TagFunc - flexible value. TagFunc is called only if the given
// tag exists in the template.
"query": TagFunc(func(w io.Writer, tag string) (int, error) {
return w.Write([]byte(url.QueryEscape(tag + "=world")))
}),
}
s := ExecuteString(template, "{{", "}}", m)
fmt.Printf("%s", s)
// Output:
// http://google.com/?foo=foobarfoobar&q=query%3Dworld&baz={{baz}}
}
func ExampleTemplateWithSpaces() {
template := "http://{{ host }}/?foo={{ bar }}{{ bar }}&q={{ query }}&baz={{ baz }}"
// Substitution map.
// Since "baz" tag is missing in the map, it will be substituted
// by an empty string.
m := map[string]interface{}{
"host": "google.com", // string - convenient
"bar": []byte("foobar"), // byte slice - the fastest
// TagFunc - flexible value. TagFunc is called only if the given
// tag exists in the template.
"query": TagFunc(func(w io.Writer, tag string) (int, error) {
return w.Write([]byte(url.QueryEscape(tag + "=world")))
}),
}
s := ExecuteString(template, "{{", "}}", m)
fmt.Printf("%s", s)
// Output:
// http://google.com/?foo=foobarfoobar&q=query%3Dworld&baz={{ baz }}
}
func ExampleTagFunc() {
template := "foo[baz]bar"
bazSlice := [][]byte{[]byte("123"), []byte("456"), []byte("789")}
m := map[string]interface{}{
// Always wrap the function into TagFunc.
//
// "baz" tag function writes bazSlice contents into w.
"baz": TagFunc(func(w io.Writer, tag string) (int, error) {
var nn int
for _, x := range bazSlice {
n, err := w.Write(x)
if err != nil {
return nn, err
}
nn += n
}
return nn, nil
}),
}
s := ExecuteString(template, "[", "]", m)
fmt.Printf("%s", s)
// Output:
// foo123456789bar
}
================================================
FILE: templating/fasttemplate/template.go
================================================
// Package fasttemplate implements simple and fast template library.
//
// Fasttemplate is faster than text/template, strings.Replace
// and strings.Replacer.
//
// Fasttemplate ideally fits for fast and simple placeholders' substitutions.
package fasttemplate
import (
"bytes"
"errors"
"fmt"
"io"
"strings"
)
// executeFunc calls f on each template tag (placeholder) occurrence.
//
// Returns the number of bytes written to w.
//
// This function is optimized for constantly changing templates.
// Use Template.ExecuteFunc for frozen templates.
func executeFunc(template, startTag, endTag string, w io.Writer, f TagFunc) (int64, error) {
var nn int64
var ni int
var err error
for {
n := strings.Index(template, startTag)
if n < 0 {
break
}
ni, err = w.Write([]byte(template[:n]))
nn += int64(ni)
if err != nil {
return nn, err
}
template = template[n+len(startTag):]
n = strings.Index(template, endTag)
if n < 0 {
// cannot find end tag - just write it to the output.
ni, err = w.Write([]byte(startTag))
nn += int64(ni)
if err != nil {
return nn, err
}
break
}
tag := template[:n]
ni, err = f(w, tag)
nn += int64(ni)
if err != nil {
if err == missingTag {
ni, err = w.Write([]byte(startTag + tag + endTag))
nn += int64(ni)
if err != nil {
return nn, err
}
} else {
return nn, err
}
}
template = template[n+len(endTag):]
}
ni, err = w.Write([]byte(template))
nn += int64(ni)
return nn, err
}
// Execute substitutes template tags (placeholders) with the corresponding
// values from the map m and writes the result to the given writer w.
//
// Substitution map m may contain values with the following types:
// * []byte - the fastest value type
// * string - convenient value type
// * TagFunc - flexible value type
//
// Returns the number of bytes written to w.
//
// This function is optimized for constantly changing templates.
// Use Template.Execute for frozen templates.
func Execute(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) {
return executeFunc(template, startTag, endTag, w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
}
// executeFuncString calls f on each template tag (placeholder) occurrence
// and substitutes it with the data written to TagFunc's w.
//
// Returns the resulting string.
//
// This function is optimized for constantly changing templates.
// Use Template.ExecuteFuncString for frozen templates.
func executeFuncString(template, startTag, endTag string, f TagFunc) string {
tagsCount := bytes.Count([]byte(template), []byte(startTag))
if tagsCount == 0 {
return template
}
bb := &bytes.Buffer{}
if _, err := executeFunc(template, startTag, endTag, bb, f); err != nil {
panic(fmt.Sprintf("unexpected error: %s", err))
}
return bb.String()
}
// ExecuteString substitutes template tags (placeholders) with the corresponding
// values from the map m and returns the result.
//
// Substitution map m may contain values with the following types:
// * []byte - the fastest value type
// * string - convenient value type
// * TagFunc - flexible value type
//
// This function is optimized for constantly changing templates.
// Use Template.ExecuteString for frozen templates.
func ExecuteString(template, startTag, endTag string, m map[string]interface{}) string {
return executeFuncString(template, startTag, endTag, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
}
// TagFunc can be used as a substitution value in the map passed to Execute*.
// Execute* functions pass tag (placeholder) name in 'tag' argument.
//
// TagFunc must be safe to call from concurrently running goroutines.
//
// TagFunc must write contents to w and return the number of bytes written.
type TagFunc func(w io.Writer, tag string) (int, error)
var missingTag = errors.New("missing tag")
func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) {
tag = strings.TrimSpace(tag)
v, exists := m[tag]
if !exists {
return 0, missingTag
}
if v == nil {
return 0, nil
}
switch value := v.(type) {
case []byte:
return w.Write(value)
case string:
return w.Write([]byte(value))
case TagFunc:
return value(w, tag)
default:
panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v))
}
}
================================================
FILE: templating/fasttemplate/template_test.go
================================================
package fasttemplate
import (
"bytes"
"io"
"testing"
)
func TestExecuteFunc(t *testing.T) {
testExecuteFunc(t, "", "")
testExecuteFunc(t, "a", "a")
testExecuteFunc(t, "abc", "abc")
testExecuteFunc(t, "{foo}", "xxxx")
testExecuteFunc(t, "a{foo}", "axxxx")
testExecuteFunc(t, "{foo}a", "xxxxa")
testExecuteFunc(t, "a{foo}bc", "axxxxbc")
testExecuteFunc(t, "{foo}{foo}", "xxxxxxxx")
testExecuteFunc(t, "{foo}{foo}", "xxxxxxxx")
testExecuteFunc(t, "{foo}bar{foo}", "xxxxbarxxxx")
// unclosed tag
testExecuteFunc(t, "{unclosed", "{unclosed")
testExecuteFunc(t, "{{unclosed", "{{unclosed")
testExecuteFunc(t, "{un{closed", "{un{closed")
// test unknown tag
testExecuteFunc(t, "{unknown}", "zz")
testExecuteFunc(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxqzzzzbarxxxx")
}
func testExecuteFunc(t *testing.T, template, expectedOutput string) {
var bb bytes.Buffer
executeFunc(template, "{", "}", &bb, func(w io.Writer, tag string) (int, error) {
if tag == "foo" {
return w.Write([]byte("xxxx"))
}
return w.Write([]byte("zz"))
})
output := string(bb.Bytes())
if output != expectedOutput {
t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput)
}
}
func TestExecute(t *testing.T) {
testExecute(t, "", "")
testExecute(t, "a", "a")
testExecute(t, "abc", "abc")
testExecute(t, "{foo}", "xxxx")
testExecute(t, "a{foo}", "axxxx")
testExecute(t, "{foo}a", "xxxxa")
testExecute(t, "a{foo}bc", "axxxxbc")
testExecute(t, "{foo}{foo}", "xxxxxxxx")
testExecute(t, "{foo}bar{foo}", "xxxxbarxxxx")
// unclosed tag
testExecute(t, "{unclosed", "{unclosed")
testExecute(t, "{{unclosed", "{{unclosed")
testExecute(t, "{un{closed", "{un{closed")
// test unknown tag
testExecute(t, "{unknown}", "{unknown}")
testExecute(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx")
testExecute(t, "{foo}q{ unexpected }{ missing }bar{foo}", "xxxxq{ unexpected }{ missing }barxxxx")
}
func testExecute(t *testing.T, template, expectedOutput string) {
var bb bytes.Buffer
Execute(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"})
output := string(bb.Bytes())
if output != expectedOutput {
t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput)
}
}
func TestExecuteString(t *testing.T) {
testExecuteString(t, "", "")
testExecuteString(t, "a", "a")
testExecuteString(t, "abc", "abc")
testExecuteString(t, "{foo}", "xxxx")
testExecuteString(t, "a{foo}", "axxxx")
testExecuteString(t, "{foo}a", "xxxxa")
testExecuteString(t, "a{foo}bc", "axxxxbc")
testExecuteString(t, "{foo}{foo}", "xxxxxxxx")
testExecuteString(t, "{foo}bar{foo}", "xxxxbarxxxx")
// unclosed tag
testExecuteString(t, "{unclosed", "{unclosed")
testExecuteString(t, "{{unclosed", "{{unclosed")
testExecuteString(t, "{un{closed", "{un{closed")
// test unknown tag
testExecuteString(t, "{unknown}", "{unknown}")
testExecuteString(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx")
testExecuteString(t, "{foo}q{ unexpected }{ missing }bar{foo}", "xxxxq{ unexpected }{ missing }barxxxx")
}
func testExecuteString(t *testing.T, template, expectedOutput string) {
output := ExecuteString(template, "{", "}", map[string]interface{}{"foo": "xxxx"})
if output != expectedOutput {
t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput)
}
}
func expectPanic(t *testing.T, f func()) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("missing panic")
}
}()
f()
}
================================================
FILE: templating/main.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package main
import (
"flag"
"io/ioutil"
"log"
"os"
"strings"
"github.com/adobe/rules_gitops/templating/fasttemplate"
)
type arrayFlags []string
func (i *arrayFlags) String() string {
return ""
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
var (
stampInfoFile arrayFlags
output, template string
variable, imports arrayFlags
executable bool
startTag, endTag string
)
func init() {
flag.Var(&stampInfoFile, "stamp_info_file", "Paths to info_file and version_file files for stamping. Content of stamp_info_file files will be used to substitute variable values so --variable VAR={BUILD_USER}/value will result in {{VAR}} being expanded to builduser/value")
flag.Var(&variable, "variable", "A variable to expand in the template, in the format NAME=VALUE")
flag.Var(&imports, "imports", "A file to import as another template, in the format NAME=filename")
flag.StringVar(&output, "output", "", "The output file")
flag.StringVar(&template, "template", "", "The input file, mandatory")
flag.BoolVar(&executable, "executable", false, "Whether to adds the executable bit to the output")
flag.StringVar(&startTag, "start_tag", "{{", "Start tag for template placeholders")
flag.StringVar(&endTag, "end_tag", "}}", "End tag for template placeholders")
}
func workspaceStatusDict(filenames []string) map[string]interface{} {
d := map[string]interface{}{}
for _, f := range filenames {
content, err := ioutil.ReadFile(f)
if err != nil {
log.Fatalf("Unable to read %s: %v", f, err)
}
for _, l := range strings.Split(string(content), "\n") {
sv := strings.SplitN(l, " ", 2)
if len(sv) == 2 {
d[sv[0]] = sv[1]
}
}
}
return d
}
func main() {
var err error
flag.Parse()
stamps := workspaceStatusDict(stampInfoFile)
ctx := map[string]interface{}{}
for _, v := range variable {
sv := strings.SplitN(v, "=", 2)
if len(sv) != 2 {
log.Fatalf("variable must be VAR=value, got %s", v)
}
val := fasttemplate.ExecuteString(sv[1], "{", "}", stamps)
ctx[sv[0]] = val
ctx["variables."+sv[0]] = val
}
for _, v := range imports {
sv := strings.SplitN(v, "=", 2)
if len(sv) != 2 {
log.Fatalf("imports must be VAR=filename, got %s", v)
}
imp, err := ioutil.ReadFile(sv[1])
if err != nil {
log.Fatalf("Unable to parse file %s: %v", sv[1], err)
}
val := fasttemplate.ExecuteString(string(imp), startTag, endTag, ctx)
// if err != nil {
// log.Fatalf("Unable to execute template %s: %v", sv[1], err)
// }
ctx["imports."+sv[0]] = fasttemplate.ExecuteString(val, "{", "}", stamps)
}
var tpl []byte
if template != "" {
tpl, err = ioutil.ReadFile(template)
if err != nil {
log.Fatalf("Unable to parse template %s: %v", template, err)
}
} else {
tpl, err = ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Unable to parse template from stdin: %v", err)
}
}
outf := os.Stdout
if output != "" {
var perm os.FileMode = 0666
if executable {
perm = 0777
}
outf, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
log.Fatalf("Unable to create output file %s: %v", output, err)
}
defer outf.Close()
}
_, err = fasttemplate.Execute(string(tpl), startTag, endTag, outf, ctx)
if err != nil {
log.Fatalf("Unable to execute template %s: %v", template, err)
}
}
================================================
FILE: templating/testdata/generated
================================================
generated data here, {{VAR}}
================================================
FILE: templating/testdata/stable-status.txt
================================================
BUILD_EMBED_LABEL
BUILD_HOST pesterni-macOS-1
BUILD_USER pesterni
================================================
FILE: templating/testdata/template1.tpl
================================================
Welcome, {{VAR}}!
{{imports.IMP}}
================================================
FILE: templating/testdata/volatile-status.txt
================================================
BUILD_SCM_REVISION cc45ebf5a259abf555a73ba9f751d954c4d26612
BUILD_SCM_SHORT_REVISION cc45ebf5a2
BUILD_SCM_STATUS Modified
BUILD_TIMESTAMP 1547146348
================================================
FILE: testing/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
bzl_library(
name = "defs",
srcs = ["defs.bzl"],
visibility = ["//visibility:public"],
deps = [
"//testing/private:k8s_test_namespace",
"//testing/private:k8s_test_setup",
],
)
================================================
FILE: testing/defs.bzl
================================================
"""Public API for Kubernetes testing rules."""
load("//testing/private:k8s_test_namespace.bzl", _k8s_test_namespace = "k8s_test_namespace")
load("//testing/private:k8s_test_setup.bzl", _k8s_test_setup = "k8s_test_setup")
k8s_test_namespace = _k8s_test_namespace
k8s_test_setup = _k8s_test_setup
================================================
FILE: testing/it_manifest_filter/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["it_manifest_filter.go"],
importpath = "github.com/adobe/rules_gitops/testing/it_manifest_filter",
visibility = ["//visibility:private"],
deps = ["//testing/it_manifest_filter/pkg:go_default_library"],
)
go_binary(
name = "it_manifest_filter",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
================================================
FILE: testing/it_manifest_filter/it_manifest_filter.go
================================================
package main
import (
"flag"
"log"
"os"
filter "github.com/adobe/rules_gitops/testing/it_manifest_filter/pkg"
)
var (
inf = flag.String("infile", "", "Input file")
outf = flag.String("outfile", "", "Out file")
)
func main() {
flag.Parse()
infile := os.Stdin
if *inf != "" {
f, err := os.Open(*inf)
if err != nil {
log.Fatalf("Unable to open file %s for reading: %s", *inf, err)
}
defer f.Close()
infile = f
}
outfile := os.Stdout
if *outf != "" {
f, err := os.Create(*outf)
if err != nil {
log.Fatalf("Unable to create file %s for reading: %s", *outf, err)
}
defer f.Close()
outfile = f
}
err := filter.ReplacePDWithEmptyDirs(infile, outfile)
if err != nil {
log.Fatalf("Unable to process: %s", err)
}
}
================================================
FILE: testing/it_manifest_filter/pkg/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["filter.go"],
importpath = "github.com/adobe/rules_gitops/testing/it_manifest_filter/pkg",
visibility = ["//visibility:public"],
deps = [
"@com_github_ghodss_yaml//:go_default_library",
"@io_k8s_api//apps/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_apimachinery//pkg/util/yaml:go_default_library",
"@io_k8s_client_go//util/jsonpath:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["filter_test.go"],
data = glob(["testdata/**"]),
deps = [
":go_default_library",
"@com_github_google_go_cmp//cmp:go_default_library",
],
)
================================================
FILE: testing/it_manifest_filter/pkg/filter.go
================================================
package filter
import (
"fmt"
"io"
"log"
"strings"
yamlenc "github.com/ghodss/yaml"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/util/jsonpath"
)
// ReplacePDWithEmptyDirs reads yaml or json stream from in, deserialize it and replace references to all PVC volumes with EmptyDir
// remove all PersistemtVolumeClaim objects
// then serialize it back into out stream.
func ReplacePDWithEmptyDirs(in io.Reader, out io.Writer) error {
decoder := yaml.NewYAMLOrJSONDecoder(in, 1024)
var err error
firstObj := true
for err == nil || isEmptyYamlError(err) {
var obj unstructured.Unstructured
err = decoder.Decode(&obj)
if err != nil {
continue
}
if obj.GetName() == "" {
return fmt.Errorf("Missing metadata.name in object %v", obj)
}
if obj.GetKind() == "" {
return fmt.Errorf("Missing kind in object %v", obj)
}
if obj.GetKind() == "PersistentVolumeClaim" {
continue // skip all PVCs
}
if obj.GetKind() == "Ingress" {
continue // skip all Ingress objects
}
if obj.GetKind() == "StatefulSet" && obj.GetAPIVersion() == "apps/v1" {
var statefulset appsv1.StatefulSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &statefulset)
if err != nil {
return fmt.Errorf("Unable to decode statefulset object %s", obj.GetName())
}
processStatefulSet(&statefulset)
obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&statefulset)
if err != nil {
return fmt.Errorf("Unable to convert statefulset to unstructured: %v", err)
}
delete(obj.Object, "status")
}
if obj.GetKind() == "Certificate" {
findAndReplaceIssuerName(obj.Object)
}
findAndReplacePVC(obj.Object)
buf, err := yamlenc.Marshal(obj.Object)
if err != nil {
return fmt.Errorf("Unable to marshal object %v", obj.Object)
}
if firstObj {
firstObj = false
} else {
_, err = out.Write([]byte("---\n"))
if err != nil {
return err
}
}
_, err = out.Write(buf)
if err != nil {
return err
}
}
if err != io.EOF {
return err
}
return nil
}
func isEmptyYamlError(err error) bool {
return strings.Contains(err.Error(), "is missing in 'null'")
}
var emptydirSpec = make(map[string]interface{})
/*
findAndReplaceTag replaces the image tags inside one object
It searches the object for container session
then loops though all images inside containers session, finds matched ones and update the tag name
*/
func findAndReplacePVC(obj map[string]interface{}) {
found := false
_, found = obj["persistentVolumeClaim"]
if found {
delete(obj, "persistentVolumeClaim")
obj["emptyDir"] = emptydirSpec
}
if !found {
findPVC(obj)
}
}
func findPVC(obj map[string]interface{}) {
for key := range obj {
switch typedV := obj[key].(type) {
case map[string]interface{}:
findAndReplacePVC(typedV)
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if ok {
findAndReplacePVC(typedItem)
}
}
}
}
}
func processStatefulSet(obj *appsv1.StatefulSet) {
if len(obj.Spec.VolumeClaimTemplates) == 0 {
return
}
//collect existing volumes
existingVolumes := make(map[string]int)
for i, v := range obj.Spec.Template.Spec.Volumes {
existingVolumes[v.Name] = i
}
for _, vct := range obj.Spec.VolumeClaimTemplates {
name := vct.GetObjectMeta().GetName()
vol := v1.Volume{
Name: name,
VolumeSource: v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
},
}
if storage, ok := vct.Spec.Resources.Requests["storage"]; ok {
vol.VolumeSource.EmptyDir.SizeLimit = &storage
}
if i, ok := existingVolumes[name]; ok {
obj.Spec.Template.Spec.Volumes[i] = vol
} else {
obj.Spec.Template.Spec.Volumes = append(obj.Spec.Template.Spec.Volumes, vol)
}
}
obj.Spec.VolumeClaimTemplates = nil
}
func findAndReplaceIssuerName(obj map[string]interface{}) {
j := jsonpath.New("cert_issuer_name")
err := j.Parse(`{.spec.issuerRef}`)
if err != nil {
log.Fatalln("Unable to parse jsonpath: ", err)
}
res, err := j.FindResults(obj)
if err != nil {
log.Println("Unable to find jsonpath: ", err)
return
}
issuerRef := res[0][0].Interface().(map[string]interface{})
if issuerRef["name"] == "letsencrypt-prod" {
issuerRef["name"] = "letsencrypt-staging"
}
}
================================================
FILE: testing/it_manifest_filter/pkg/filter_test.go
================================================
package filter_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
filter "github.com/adobe/rules_gitops/testing/it_manifest_filter/pkg"
"github.com/google/go-cmp/cmp"
)
func TestHappyPath(t *testing.T) {
testcases := []string{"happypath", "statefulset", "statefulset2", "certificate"}
for _, testcase := range testcases {
t.Run(testcase, func(t *testing.T) {
infn := fmt.Sprintf("testdata/%s.yaml", testcase)
expectedfn := fmt.Sprintf("testdata/%s.expected.yaml", testcase)
inf, err := os.Open(infn)
if err != nil {
t.Errorf("Unable to open file %s", infn)
return
}
defer inf.Close()
expectedb, err := ioutil.ReadFile(expectedfn)
if err != nil {
t.Errorf("Unable to read file %s", expectedfn)
return
}
expected := strings.TrimSpace(string(expectedb))
var outbuf bytes.Buffer
err = filter.ReplacePDWithEmptyDirs(inf, &outbuf)
if err != nil {
t.Errorf("Unexpected error %v", err)
return
}
if diff := cmp.Diff(expected, strings.TrimSpace(outbuf.String())); diff != "" {
t.Errorf("Unexpected output (-want +got):\n%s", diff)
}
})
}
}
================================================
FILE: testing/it_manifest_filter/pkg/testdata/certificate.expected.yaml
================================================
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: actv-notification-certificate
namespace: namespace
spec:
acme:
config:
- dns01:
provider: route53
domains:
- actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
commonName: actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
dnsNames:
- actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
issuerRef:
kind: ClusterIssuer
name: letsencrypt-staging
secretName: actv-notification-tls
================================================
FILE: testing/it_manifest_filter/pkg/testdata/certificate.yaml
================================================
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: actv-notification-certificate
namespace: namespace
spec:
secretName: actv-notification-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
dnsNames:
- actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
acme:
config:
- dns01:
provider: route53
domains:
- actv-notification-service.@k8s.namespace@.@k8s.cluster@.k8s.tubemogul.info
================================================
FILE: testing/it_manifest_filter/pkg/testdata/happypath.expected.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: pesterni
spec:
ports:
- port: 3306
selector:
app: mariadb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
namespace: pesterni
spec:
replicas: 1
selector:
matchLabels:
app: mariadb
strategy:
type: Recreate
template:
metadata:
labels:
app: mariadb
spec:
containers:
- env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: mysql-secret
image: cr.k8s.tubemogul.info/pesterni/db/image@sha256:f4504709ea0a2f3a8f0ba29de5ef7cdc6e3be1c4411d39a60058861c16e6a5aa
name: mariadb
ports:
- containerPort: 3306
name: mysql
readinessProbe:
exec:
command:
- mysql
- -h
- 127.0.0.1
- -e
- SELECT 1
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 1
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 10m
memory: 1Gi
volumeMounts:
- mountPath: /data
name: data-volume
volumes:
- emptyDir: {}
name: data-volume
================================================
FILE: testing/it_manifest_filter/pkg/testdata/happypath.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: pesterni
spec:
ports:
- port: 3306
selector:
app: mariadb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
namespace: pesterni
spec:
replicas: 1
selector:
matchLabels:
app: mariadb
strategy:
type: Recreate
template:
metadata:
labels:
app: mariadb
spec:
containers:
- env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: mysql-secret
image: cr.k8s.tubemogul.info/pesterni/db/image@sha256:f4504709ea0a2f3a8f0ba29de5ef7cdc6e3be1c4411d39a60058861c16e6a5aa
name: mariadb
ports:
- containerPort: 3306
name: mysql
readinessProbe:
exec:
command:
- mysql
- -h
- 127.0.0.1
- -e
- SELECT 1
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 1
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 10m
memory: 1Gi
volumeMounts:
- mountPath: /data
name: data-volume
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: mariadb-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
volume.beta.kubernetes.io/storage-class: standard
name: mariadb-data
namespace: pesterni
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset.expected.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
creationTimestamp: null
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
volumes:
- emptyDir:
sizeLimit: 5Gi
name: datadir
updateStrategy:
type: RollingUpdate
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: "0.5"
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
volumes:
- emptyDir: {}
name: datadir
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
annotations:
volume.beta.kubernetes.io/storage-class: standard
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
name: datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset2.expected.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
creationTimestamp: null
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
volumes:
- emptyDir:
sizeLimit: 5Gi
name: datadir
- emptyDir: {}
name: datadir2
updateStrategy:
type: RollingUpdate
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset2.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: "0.5"
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
volumes:
- emptyDir: {}
name: datadir
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
annotations:
volume.beta.kubernetes.io/storage-class: standard
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
name: datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
- metadata:
annotations:
volume.beta.kubernetes.io/storage-class: standard
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
name: datadir2
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset3.expected.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
creationTimestamp: null
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
volumes:
- emptyDir:
sizeLimit: 5Gi
name: datadir
updateStrategy:
type: RollingUpdate
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_manifest_filter/pkg/testdata/statefulset3.yaml
================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
namespace: actv-apps-uat
spec:
podManagementPolicy: Parallel
replicas: 3
selector:
matchLabels:
app: zk
serviceName: zk-hs
template:
metadata:
labels:
app: zk
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- zk
topologyKey: kubernetes.io/hostname
containers:
- command:
- sh
- -c
- start-zookeeper --servers=3 --data_dir=/var/lib/zookeeper/data --data_log_dir=/var/lib/zookeeper/data
--conf_dir=/opt/zookeeper/conf --client_port=2181 --election_port=3888 --server_port=2888
--tick_time=2000 --init_limit=10 --sync_limit=5 --heap=512M --max_client_cnxns=60
--snap_retain_count=3 --purge_interval=1 --max_session_timeout=40000 --min_session_timeout=4000
--log_level=ERROR
image: k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
livenessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
name: kubernetes-zookeeper
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
readinessProbe:
exec:
command:
- sh
- -c
- zookeeper-ready 2181
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: "0.5"
memory: 1Gi
volumeMounts:
- mountPath: /var/lib/zookeeper
name: datadir
securityContext:
fsGroup: 1000
runAsUser: 1000
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
annotations:
volume.beta.kubernetes.io/storage-class: standard
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs
name: datadir
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
namespace: actv-apps-uat
spec:
maxUnavailable: 1
selector:
matchLabels:
app: zk
================================================
FILE: testing/it_sidecar/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["it_sidecar.go"],
importpath = "github.com/adobe/rules_gitops/testing/it_sidecar",
visibility = ["//visibility:private"],
deps = [
"//testing/it_sidecar/stern:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_client_go//informers:go_default_library",
"@io_k8s_client_go//kubernetes:go_default_library",
"@io_k8s_client_go//plugin/pkg/client/auth/gcp:go_default_library",
"@io_k8s_client_go//rest:go_default_library",
"@io_k8s_client_go//tools/cache:go_default_library",
"@io_k8s_client_go//tools/clientcmd:go_default_library",
"@io_k8s_client_go//tools/portforward:go_default_library",
"@io_k8s_client_go//transport/spdy:go_default_library",
"@io_k8s_client_go//util/homedir:go_default_library",
],
)
go_binary(
name = "it_sidecar",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
================================================
FILE: testing/it_sidecar/client/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library")
exports_files(["noop.setup"])
go_library(
name = "go_default_library",
srcs = ["sidecar_client.go"],
data = [
":noop.setup",
],
importpath = "github.com/adobe/rules_gitops/testing/it_sidecar/client",
visibility = ["//visibility:public"],
)
================================================
FILE: testing/it_sidecar/client/noop.setup
================================================
#! /bin/bash
# This is a no-op setup script that simulates the K8Setup script output
echo "FORWARD foo:8000:8000"
echo "FORWARD bar:8001:8001"
echo "READY"
================================================
FILE: testing/it_sidecar/client/sidecar_client.go
================================================
package client
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"testing"
)
// K8STestSetup is instantiated given the pods and services we must wait for
type K8STestSetup struct {
WaitForPods []string
PortForwardServices map[string]int
forwards map[string]int
cmd *exec.Cmd
in io.WriteCloser
out io.ReadCloser
er io.ReadCloser
// ReadyCallback for custom setup after services are ready and before pre-test
ReadyCallback Callback
}
// Callback function type is invoked post-setup but pre-test
type Callback func() error
var setupCMD = flag.String("setup", "", "the path to the it setup command")
// TestMain will execute the provided setup command, wait for configured pods and services to be
// ready, and then forwards service logs to test output. On completion, signals to the it_sidecar
// to teardown the test namespace
func (s *K8STestSetup) TestMain(m *testing.M) {
s.forwards = make(map[string]int)
wg := new(sync.WaitGroup)
wg.Add(2) // there will be 2 goroutines, one reading stdout and one reading stdin
os.Exit(func() int {
flag.Parse()
// Defer sidecar process tear-down.
defer func() {
//Closing standard-in pipe signals sidecar process to exit,
s.in.Close()
wg.Wait() // Wait for reader goroutines to actually finish
if err := s.cmd.Wait(); err != nil {
log.Fatal(err)
}
}()
s.before(wg)
if s.ReadyCallback != nil {
err := s.ReadyCallback()
if err != nil {
log.Fatal(err)
}
}
// Run tests.
return m.Run()
}())
}
func (s *K8STestSetup) GetServiceLocalPort(serviceName string) int {
return s.forwards[serviceName]
}
func (s *K8STestSetup) before(wg *sync.WaitGroup) {
log.Printf("setup command: %s\n", *setupCMD)
args := make([]string, 0)
for _, app := range s.WaitForPods {
args = append(args, fmt.Sprintf("-waitforapp=%s", app))
}
for service, port := range s.PortForwardServices {
args = append(args, fmt.Sprintf("-portforward=%s:%d", service, port))
}
s.cmd = exec.Command(*setupCMD, args...)
var err error
// Open and start reading stderr in a new goroutine. StderrPipe will be closed automatically by the call to Wait
// so we do not need to close this ourselves. We must also guarantee that all reads on this pipe are completed
// before calling wait, so the goroutines below must be canceled before the defered teardown above
if s.er, err = s.cmd.StderrPipe(); err != nil {
log.Fatal(fmt.Errorf("unable to read setup command STDOUT; %w", err))
}
go func() {
rd := bufio.NewReader(s.er)
for {
str, err := rd.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Print(str)
}
wg.Done()
}()
//Open stdin and stdout
if s.out, err = s.cmd.StdoutPipe(); err != nil {
log.Fatal(err)
}
if s.in, err = s.cmd.StdinPipe(); err != nil {
log.Fatal(err)
}
//Start the sidecar process
if err := s.cmd.Start(); err != nil {
log.Fatal(err)
}
//Wait for all pods to be ready
rd := bufio.NewReader(s.out)
waitForReady:
for {
str, err := rd.ReadString('\n')
if err != nil {
log.Fatal("Unable to read from setup script stdout. Cannot wait for pods")
}
fmt.Print(str)
if strings.HasPrefix(str, "FORWARD") {
// remove the "FORWARD " prefix, and any trailing space, split on ":"
parts := strings.Split(strings.TrimSpace(str[8:]), ":")
localPort, _ := strconv.Atoi(parts[2])
s.forwards[parts[0]] = localPort
}
if "READY\n" == str {
break waitForReady
}
}
//Start reading stdout in a new goroutine
go func() {
for {
str, err := rd.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Print(str)
}
wg.Done()
}()
}
================================================
FILE: testing/it_sidecar/client/test_callback/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_test")
go_test(
name = "go_default_test",
srcs = ["sidecar_client_test.go"],
args = [
"-setup",
"$(location //testing/it_sidecar/client:noop.setup)",
],
data = [
"//testing/it_sidecar/client:noop.setup",
],
rundir = ".",
deps = ["//testing/it_sidecar/client:go_default_library"],
)
================================================
FILE: testing/it_sidecar/client/test_callback/sidecar_client_test.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package test_callback
import (
"github.com/adobe/rules_gitops/testing/it_sidecar/client"
"testing"
)
var (
setup client.K8STestSetup
isCallbackRun bool
)
func TestMain(m *testing.M) {
callback := func() error {
isCallbackRun = true
return nil
}
setup := client.K8STestSetup{
PortForwardServices: map[string]int{},
ReadyCallback: callback,
}
setup.TestMain(m)
}
// TestReadyCallback validates that the pre-test ReadyCallback is run. Note that this test scenario assumes
// that a K8STestSetup in TestMain will invoke the test.
func TestReadyCallback(t *testing.T) {
if !isCallbackRun {
t.Fatalf("ready callback should have been run")
}
}
================================================
FILE: testing/it_sidecar/client/test_no_callback/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["sidecar_client_test_no_callback.go"],
importpath = "github.com/adobe/rules_gitops/testing/it_sidecar/client/test_no_callback",
visibility = ["//visibility:public"],
deps = ["//testing/it_sidecar/client:go_default_library"],
)
================================================
FILE: testing/it_sidecar/client/test_no_callback/sidecar_client_test_no_callback.go
================================================
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
package test_no_callback
import (
"github.com/adobe/rules_gitops/testing/it_sidecar/client"
"testing"
)
var (
setup client.K8STestSetup
)
// The setup should run without error since ReadyCallback is optional
func TestMain(m *testing.M) {
setup := client.K8STestSetup{
PortForwardServices: map[string]int{},
}
setup.TestMain(m)
}
================================================
FILE: testing/it_sidecar/it_sidecar.go
================================================
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/adobe/rules_gitops/testing/it_sidecar/stern"
v1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
"k8s.io/client-go/util/homedir"
)
type portForwardConf struct {
services map[string][]uint16
}
func (i *portForwardConf) String() string {
return fmt.Sprintf("%v", i.services)
}
func (i *portForwardConf) Set(value string) error {
v := strings.SplitN(value, ":", 2)
if len(v) != 2 {
return fmt.Errorf("incorrect portforward '%s': must be in form of service:port", value)
}
port, err := strconv.ParseUint(v[1], 10, 16)
if err != nil {
return fmt.Errorf("incorrect port in portforward '%s': %v", value, err)
}
i.services[v[0]] = append(i.services[v[0]], uint16(port))
return nil
}
type arrayFlags []string
func (i *arrayFlags) String() string {
return "my string representation"
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
var (
namespace = flag.String("namespace", os.Getenv("NAMESPACE"), "kubernetes namespace")
timeout = flag.Duration("timeout", time.Second*30, "execution timeout")
deleteNamespace = flag.Bool("delete_namespace", false, "delete namespace as part of the cleanup")
pfconfig = portForwardConf{services: make(map[string][]uint16)}
signalChannel chan os.Signal
kubeconfig string
waitForApps arrayFlags
)
func init() {
flag.Var(&pfconfig, "portforward", "set a port forward item in form of servicename:port")
flag.StringVar(&kubeconfig, "kubeconfig", os.Getenv("KUBECONFIG"), "path to kubernetes config file")
flag.Var(&waitForApps, "waitforapp", "wait for pods with label app=")
}
// contains returns true if slice v contains an item
func contains(v []string, item string) bool {
for _, s := range v {
if s == item {
return true
}
}
return false
}
// listReadyApps converts a list returned from podsInformer.GetStore().List() to a map containing apps with ready status
// app is determined by app label
func listReadyApps(list []interface{}) (readypods, notReady []string) {
var readyApps []string
for _, it := range list {
pod, ok := it.(*v1.Pod)
if !ok {
panic(errors.New("expected pod in informer"))
}
for _, cond := range pod.Status.Conditions {
if cond.Type == v1.PodReady {
if cond.Status == v1.ConditionTrue {
readypods = append(readypods, pod.Name)
app := pod.GetLabels()["app"]
if app != "" {
readyApps = append(readyApps, app)
}
app = pod.GetLabels()["app.kubernetes.io/name"]
if app != "" {
readyApps = append(readyApps, app)
}
}
}
}
}
for _, app := range waitForApps {
if !contains(readyApps, app) {
notReady = append(notReady, app)
}
}
return
}
func waitForPods(ctx context.Context, clientset *kubernetes.Clientset) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
events := make(chan interface{})
fn := func(obj interface{}) {
events <- obj
}
handler := &cache.ResourceEventHandlerFuncs{
AddFunc: fn,
DeleteFunc: fn,
UpdateFunc: func(old interface{}, new interface{}) {
fn(new)
},
}
kubeInformerFactory := informers.NewFilteredSharedInformerFactory(clientset, time.Second*30, *namespace, nil)
podsInformer := kubeInformerFactory.Core().V1().Pods().Informer()
podsInformer.AddEventHandler(handler)
go kubeInformerFactory.Start(ctx.Done())
waitForPodsUp:
for {
select {
case <-events:
v := podsInformer.GetStore().List()
ready, notReady := listReadyApps(v)
log.Print("ready pods:", ready)
if len(notReady) != 0 {
log.Print("waiting for apps:", notReady)
} else {
log.Println("all apps are ready")
break waitForPodsUp
}
case <-ctx.Done():
return errors.New("timed out waiting for apps")
}
}
return nil
}
// listReadyServices converts a list returned from endpointsInformer.GetStore().List() to a list of services with ready status
func listReadyServices(list []interface{}) (ready, notReady []string) {
for _, it := range list {
ep, ok := it.(*v1.Endpoints)
if !ok {
panic(errors.New("expected EndpointsList in informer"))
}
for _, subset := range ep.Subsets {
if len(subset.Addresses) > 0 {
ready = append(ready, ep.Name)
break
}
}
}
for service, _ := range pfconfig.services {
if !contains(ready, service) {
notReady = append(notReady, service)
}
}
return
}
func waitForEndpoints(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config) error {
events := make(chan interface{})
fn := func(obj interface{}) {
events <- obj
}
handler := &cache.ResourceEventHandlerFuncs{
AddFunc: fn,
DeleteFunc: fn,
UpdateFunc: func(old interface{}, new interface{}) {
fn(new)
},
}
kubeInformerFactory := informers.NewFilteredSharedInformerFactory(clientset, time.Second*30, *namespace, nil)
endpointsInformer := kubeInformerFactory.Core().V1().Endpoints().Informer()
endpointsInformer.AddEventHandler(handler)
go kubeInformerFactory.Start(ctx.Done())
allReadyServices := make(map[string]bool)
waitForServicesUp:
for {
select {
case <-events:
v := endpointsInformer.GetStore().List()
ready, notReady := listReadyServices(v)
log.Print("ready services:", ready)
for _, svc := range ready {
if !allReadyServices[svc] {
allReadyServices[svc] = true
log.Print("SERVICE_READY ", svc)
if ports := pfconfig.services[svc]; len(ports) > 0 {
err := portForward(ctx, clientset, config, svc, ports)
if err != nil {
return err
}
}
}
}
if len(notReady) != 0 {
log.Print("waiting for endpoints:", notReady)
} else {
log.Println("all services are ready")
break waitForServicesUp
}
case <-ctx.Done():
return errors.New("timed out waiting for services")
}
}
return nil
}
func portForward(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, serviceName string, ports []uint16) error {
// port forward
var wg sync.WaitGroup
wg.Add(len(ports))
for _, port := range ports {
ep, err := clientset.CoreV1().Endpoints(*namespace).Get(ctx, serviceName, meta_v1.GetOptions{})
if err != nil {
return fmt.Errorf("error listing endpoints for service %s: %v", serviceName, err)
}
var podnamespace, podname string
for _, subset := range ep.Subsets {
if len(subset.Addresses) == 0 {
continue
}
podnamespace = subset.Addresses[0].TargetRef.Namespace
podname = subset.Addresses[0].TargetRef.Name
break
}
if podnamespace == "" || podname == "" {
return fmt.Errorf("no pods are available for service %s", serviceName)
}
log.Printf("%s -> %s/%s", serviceName, podnamespace, podname)
url := clientset.CoreV1().RESTClient().Post().Resource("pods").Namespace(podnamespace).Name(podname).SubResource("portforward").URL()
transport, upgrader, err := spdy.RoundTripperFor(config)
if err != nil {
return fmt.Errorf("Could not create round tripper: %v", err)
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url)
ports := []string{fmt.Sprintf(":%d", port)}
readyChan := make(chan struct{}, 1)
pf, err := portforward.New(dialer, ports, ctx.Done(), readyChan, os.Stderr, os.Stderr)
if err != nil {
return fmt.Errorf("Could not port forward into pod: %v", err)
}
go func(port uint16) {
err := pf.ForwardPorts()
if err != nil {
log.Fatalf("Could not forward ports for %s:%d : %v", serviceName, port, err)
}
}(port)
go func(port uint16) {
<-pf.Ready
ports, err := pf.GetPorts()
if err != nil {
log.Fatalf("Could not get forwarded ports for %s:%d : %v", serviceName, port, err)
}
for _, port := range ports {
fmt.Printf("FORWARD %s:%d:%d\n", serviceName, port.Remote, port.Local)
}
wg.Done()
}(port)
}
wg.Wait()
return nil
}
func cleanup(clientset *kubernetes.Clientset) {
log.Print("Cleanup")
if *deleteNamespace && *namespace != "" {
log.Printf("deleting namespace %s", *namespace)
s := meta_v1.DeletePropagationBackground
if err := clientset.CoreV1().Namespaces().Delete(context.Background(), *namespace, meta_v1.DeleteOptions{PropagationPolicy: &s}); err != nil {
log.Printf("Unable to delete namespace %s: %v", *namespace, err)
}
}
}
func main() {
flag.Parse()
log.SetOutput(os.Stdout)
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
signalChannel = make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
defer func() {
signal.Stop(signalChannel)
cancel()
}()
// cancel context if signal is received
go func() {
select {
case <-signalChannel:
cancel()
case <-ctx.Done():
}
}()
// cancel context if stdin is closed
go func() {
reader := bufio.NewReader(os.Stdin)
for {
_, _, err := reader.ReadRune()
if err != nil && err == io.EOF {
cancel()
break
}
}
}()
var clientset *kubernetes.Clientset
if kubeconfig == "" {
_, ok := os.LookupEnv("KUBERNETES_SERVICE_HOST")
if !ok {
kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config")
}
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatal(err)
}
clientset = kubernetes.NewForConfigOrDie(config)
defer cleanup(clientset)
go stern.Run(ctx, *namespace, clientset)
if len(waitForApps) > 0 {
err = waitForPods(ctx, clientset)
if err != nil {
log.Print(err)
return
}
}
if len(pfconfig.services) > 0 {
err = waitForEndpoints(ctx, clientset, config)
if err != nil {
log.Print(err)
return
}
}
fmt.Println("READY")
<-ctx.Done()
}
================================================
FILE: testing/it_sidecar/stern/BUILD.bazel
================================================
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"container_state.go",
"main.go",
"tail.go",
"watch.go",
],
importpath = "github.com/adobe/rules_gitops/testing/it_sidecar/stern",
visibility = ["//visibility:public"],
deps = [
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/labels:go_default_library",
"@io_k8s_apimachinery//pkg/watch:go_default_library",
"@io_k8s_client_go//kubernetes:go_default_library",
"@io_k8s_client_go//kubernetes/typed/core/v1:go_default_library",
"@io_k8s_client_go//rest:go_default_library",
],
)
================================================
FILE: testing/it_sidecar/stern/container_state.go
================================================
// Copyright 2016 Wercker Holding BV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stern
import (
"errors"
"k8s.io/api/core/v1"
)
type ContainerState string
const (
RUNNING = "running"
WAITING = "waiting"
TERMINATED = "terminated"
)
func NewContainerState(stateConfig string) (ContainerState, error) {
if stateConfig == RUNNING {
return RUNNING, nil
} else if stateConfig == WAITING {
return WAITING, nil
} else if stateConfig == TERMINATED {
return TERMINATED, nil
}
return "", errors.New("containerState should be one of 'running', 'waiting', or 'terminated'")
}
func (stateConfig ContainerState) Match(containerState v1.ContainerState) bool {
return (stateConfig == RUNNING && containerState.Running != nil) ||
(stateConfig == WAITING && containerState.Waiting != nil) ||
(stateConfig == TERMINATED && containerState.Terminated != nil)
}
================================================
FILE: testing/it_sidecar/stern/main.go
================================================
// Copyright 2016 Wercker Holding BV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stern
import (
"context"
"fmt"
"regexp"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
// Run starts the main run loop
func Run(ctx context.Context, namespace string, clientset *kubernetes.Clientset) error {
added, removed, err := Watch(ctx, clientset.CoreV1().Pods(namespace), regexp.MustCompile(".*"), regexp.MustCompile(".*"), RUNNING, labels.Everything())
if err != nil {
return fmt.Errorf("failed to set up watch: %v", err)
}
tails := make(map[string]*Tail)
go func() {
for p := range added {
id := p.GetID()
if tails[id] != nil {
continue
}
tail := NewTail(p.Namespace, p.Pod, p.Container)
tails[id] = tail
tail.Start(ctx, clientset.CoreV1().Pods(p.Namespace))
}
}()
go func() {
for p := range removed {
id := p.GetID()
if tails[id] == nil {
continue
}
tails[id].Close()
delete(tails, id)
}
}()
<-ctx.Done()
return nil
}
================================================
FILE: testing/it_sidecar/stern/tail.go
================================================
// Copyright 2016 Wercker Holding BV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stern
import (
"bufio"
"context"
"fmt"
"log"
"os"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
)
type Tail struct {
Namespace string
PodName string
ContainerName string
req *rest.Request
closed chan struct{}
}
// NewTail returns a new tail for a Kubernetes container inside a pod
func NewTail(namespace, podName, containerName string) *Tail {
return &Tail{
Namespace: namespace,
PodName: podName,
ContainerName: containerName,
closed: make(chan struct{}),
}
}
// Start starts tailing
func (t *Tail) Start(ctx context.Context, i v1.PodInterface) {
go func() {
fmt.Fprintf(os.Stderr, "+ %s/%s\n", t.PodName, t.ContainerName)
req := i.GetLogs(t.PodName, &corev1.PodLogOptions{
Follow: true,
Timestamps: true,
Container: t.ContainerName,
})
stream, err := req.Stream(ctx)
if err != nil {
log.Printf("Error opening stream to %s/%s/%s: %s", t.Namespace, t.PodName, t.ContainerName, err)
return
}
defer stream.Close()
go func() {
<-t.closed
stream.Close()
}()
reader := bufio.NewReader(stream)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
return
}
str := string(line)
t.Print(str)
}
}()
go func() {
<-ctx.Done()
close(t.closed)
}()
}
// Close stops tailing
func (t *Tail) Close() {
fmt.Fprintf(os.Stderr, "Log finished %s\n", t.PodName)
close(t.closed)
}
// Print prints a color coded log message with the pod and container names
func (t *Tail) Print(msg string) {
fmt.Fprintf(os.Stderr, "[%s/%s]: %s", t.PodName, t.ContainerName, msg)
}
================================================
FILE: testing/it_sidecar/stern/watch.go
================================================
// Copyright 2016 Wercker Holding BV
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stern
import (
"context"
"fmt"
"regexp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/typed/core/v1"
)
// Target is a target to watch
type Target struct {
Namespace string
Pod string
Container string
}
// GetID returns the ID of the object
func (t *Target) GetID() string {
return fmt.Sprintf("%s-%s-%s", t.Namespace, t.Pod, t.Container)
}
// Watch starts listening to Kubernetes events and emits modified
// containers/pods. The first result is targets added, the second is targets
// removed
func Watch(ctx context.Context, i v1.PodInterface, podFilter *regexp.Regexp, containerFilter *regexp.Regexp, containerState ContainerState, labelSelector labels.Selector) (chan *Target, chan *Target, error) {
watcher, err := i.Watch(ctx, metav1.ListOptions{Watch: true, LabelSelector: labelSelector.String()})
if err != nil {
return nil, nil, fmt.Errorf("failed to set up watch: %s", err)
}
added := make(chan *Target)
removed := make(chan *Target)
go func() {
for {
select {
case e := <-watcher.ResultChan():
if e.Object == nil {
// Closed because of error
return
}
var (
pod *corev1.Pod
ok bool
)
if pod, ok = e.Object.(*corev1.Pod); !ok {
continue
}
if !podFilter.MatchString(pod.Name) {
continue
}
switch e.Type {
case watch.Added, watch.Modified:
var statuses []corev1.ContainerStatus
statuses = append(statuses, pod.Status.InitContainerStatuses...)
statuses = append(statuses, pod.Status.ContainerStatuses...)
for _, c := range statuses {
if !containerFilter.MatchString(c.Name) {
continue
}
// if containerExcludeFilter != nil && containerExcludeFilter.MatchString(c.Name) {
// continue
// }
if containerState.Match(c.State) {
added <- &Target{
Namespace: pod.Namespace,
Pod: pod.Name,
Container: c.Name,
}
}
}
case watch.Deleted:
var containers []corev1.Container
containers = append(containers, pod.Spec.Containers...)
containers = append(containers, pod.Spec.InitContainers...)
for _, c := range containers {
if !containerFilter.MatchString(c.Name) {
continue
}
// if containerExcludeFilter != nil && containerExcludeFilter.MatchString(c.Name) {
// continue
// }
removed <- &Target{
Namespace: pod.Namespace,
Pod: pod.Name,
Container: c.Name,
}
}
}
case <-ctx.Done():
watcher.Stop()
close(added)
close(removed)
return
}
}
}()
return added, removed, nil
}
================================================
FILE: testing/private/BUILD.bazel
================================================
load("@bazel_lib//:bzl_library.bzl", "bzl_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("//kustomize:defs.bzl", "kustomize_binary")
exports_files([
"k8s_test_namespace.sh.tpl",
])
kustomize_binary(
name = "kustomize_bin",
)
sh_binary(
name = "set_namespace",
srcs = ["set_namespace.sh"],
data = [":kustomize_bin"],
visibility = [
"//testing/private:__subpackages__",
"//tests:__subpackages__",
],
deps = ["@bazel_tools//tools/bash/runfiles"],
)
bzl_library(
name = "k8s_test_setup",
srcs = ["k8s_test_setup.bzl"],
visibility = ["//testing:__subpackages__"],
deps = [
"//adapters:providers",
"//kustomize:defs",
],
)
bzl_library(
name = "k8s_test_namespace",
srcs = ["k8s_test_namespace.bzl"],
visibility = ["//testing:__subpackages__"],
)
================================================
FILE: testing/private/k8s_test_namespace.bzl
================================================
"""Rule for creating Kubernetes test namespaces."""
def _k8s_test_namespace_impl(ctx):
files = [] # runfiles list
# add files referenced by rule attributes to runfiles
files = [ctx.file.kubectl, ctx.file.kubeconfig]
# create namespace reservation script
namespace_create = ctx.actions.declare_file(ctx.label.name + ".create")
ctx.actions.expand_template(
template = ctx.file._namespace_template,
substitutions = {
"%{kubeconfig}": ctx.file.kubeconfig.path,
"%{kubectl}": ctx.file.kubectl.path,
},
output = namespace_create,
is_executable = True,
)
files.append(namespace_create)
return [DefaultInfo(
executable = namespace_create,
runfiles = ctx.runfiles(files = files),
)]
k8s_test_namespace = rule(
attrs = {
"kubeconfig": attr.label(
allow_single_file = True,
),
"kubectl": attr.label(
cfg = "exec",
executable = True,
allow_single_file = True,
),
"_namespace_template": attr.label(
default = Label("//testing/private:k8s_test_namespace.sh.tpl"),
allow_single_file = True,
),
},
executable = True,
implementation = _k8s_test_namespace_impl,
)
================================================
FILE: testing/private/k8s_test_namespace.sh.tpl
================================================
#!/usr/bin/env bash
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
# TODO: disable trace
set -euo pipefail
[ -o xtrace ] && env
function guess_runfiles() {
pushd ${BASH_SOURCE[0]}.runfiles > /dev/null 2>&1
pwd
popd > /dev/null 2>&1
}
RUNFILES=${TEST_SRCDIR:-$(guess_runfiles)}
TEST_UNDECLARED_OUTPUTS_DIR=${TEST_UNDECLARED_OUTPUTS_DIR:-.}
KUBECTL=%{kubectl}
KUBECONFIG=%{kubeconfig}
CLUSTER_FILE=%{cluster}
SET_NAMESPACE=%{set_namespace}
IT_MANIFEST_FILTER=%{it_manifest_filter}
NAMESPACE_NAME_FILE=${TEST_UNDECLARED_OUTPUTS_DIR}/namespace
KUBECONFIG_FILE=${TEST_UNDECLARED_OUTPUTS_DIR}/kubeconfig
# get cluster and username from provided configuration
CLUSTER=$(cat ${CLUSTER_FILE})
USER=$(${KUBECTL} --kubeconfig=${KUBECONFIG} config view -o jsonpath='{.users[?(@.name == '"\"${CLUSTER}\")].name}")
echo "Cluster: ${CLUSTER}" >&2
echo "User: ${USER}" >&2
set +e
if [ -n "${K8S_MYNAMESPACE:-}" ]
then
# do not create random namesspace
NAMESPACE=${USER}
# do not delete namespace after the test is complete
DELETE_NAMESPACE_FLAG=""
else
# create random namespace
DELETE_NAMESPACE_FLAG="-delete_namespace"
COUNT="0"
while true; do
NAMESPACE=`whoami`-$(( (RANDOM) + 32767 ))
${KUBECTL} --kubeconfig=${KUBECONFIG} --cluster=${CLUSTER} --user=${USER} create namespace ${NAMESPACE} && break
COUNT=$[$COUNT + 1]
if [ $COUNT -ge 10 ]; then
echo "Unable to create namespace in $COUNT attempts!" >&2
exit 1
fi
done
fi
echo "Namespace: ${NAMESPACE}" >&2
set -e
# expose generated namespace name as rule output
mkdir -p $(dirname $NAMESPACE_NAME_FILE)
echo $NAMESPACE > $NAMESPACE_NAME_FILE
# create kubectl configuration copy with default context set to use newly created namespace
mkdir -p $(dirname $KUBECONFIG_FILE)
cat ${KUBECONFIG} > $KUBECONFIG_FILE
export KUBECONFIG=$KUBECONFIG_FILE
CONTEXT=$CLUSTER-$NAMESPACE
${KUBECTL} --cluster=$CLUSTER --user=$USER --namespace=$NAMESPACE config set-context $CONTEXT >&2
${KUBECTL} config use-context $CONTEXT >&2
# set runfiles for STMTS
export PYTHON_RUNFILES=${RUNFILES}
PIDS=()
function async() {
# Launch the command asynchronously and track its process id.
PYTHON_RUNFILES=${RUNFILES} "$@" &
PIDS+=($!)
}
function waitpids() {
# Wait for all of the subprocesses, failing the script if any of them failed.
if [ "${#PIDS[@]}" != 0 ]; then
for pid in ${PIDS[@]}; do
wait ${pid}
done
fi
}
%{push_statements}
# create k8s objects
%{statements}
%{it_sidecar} -namespace=${NAMESPACE} -timeout=%{test_timeout} %{portforwards} %{waitforapps} ${DELETE_NAMESPACE_FLAG} "$@"
================================================
FILE: testing/private/k8s_test_setup.bzl
================================================
"""Rule for setting up Kubernetes test environments."""
load("//adapters:providers.bzl", "K8sPushInfo")
load("//kustomize:defs.bzl", "KustomizeInfo")
def _image_push_statements(
_ctx,
kustomize_objs,
files = []):
statements = ""
trans_img_pushes = depset(transitive = [obj[KustomizeInfo].image_pushes for obj in kustomize_objs]).to_list()
statements += "\n".join([
"echo pushing {}/{}".format(exe[K8sPushInfo].registry, exe[K8sPushInfo].repository)
for exe in trans_img_pushes
]) + "\n"
statements += "\n".join([
"async \"%s\"" % exe.files_to_run.executable.short_path
for exe in trans_img_pushes
]) + "\nwaitpids\n"
files += [obj.files_to_run.executable for obj in trans_img_pushes]
dep_runfiles = [obj[DefaultInfo].default_runfiles for obj in trans_img_pushes]
return statements, files, dep_runfiles
def _k8s_test_setup_impl(ctx):
files = [] # runfiles list
transitive = []
commands = [] # the list of commands to execute
kustomize_executable = ctx.toolchains["@rules_gitops//kustomize:toolchain_type"].kustomizeinfo.executable
# add files referenced by rule attributes to runfiles
files = [ctx.executable._stamper, ctx.file.kubectl, ctx.file.kubeconfig, kustomize_executable, ctx.executable._it_sidecar, ctx.executable._it_manifest_filter]
files += ctx.files._set_namespace
files += ctx.files.cluster
push_statements, files, pushes_runfiles = _image_push_statements(ctx, [o for o in ctx.attr.objects if KustomizeInfo in o], files)
# execute all objects targets
for obj in ctx.attr.objects:
if obj.files_to_run.executable:
# add object' targets and excutables to runfiles
files.append(obj.files_to_run.executable)
transitive.append(obj.default_runfiles.files)
# add object' execution command
commands.append(obj.files_to_run.executable.short_path + " | ${SET_NAMESPACE} $NAMESPACE | ${IT_MANIFEST_FILTER} | ${KUBECTL} apply -f -")
else:
files += obj.files.to_list()
commands += [ctx.executable._template_engine.short_path + " --template=" + filename.short_path + " --variable=NAMESPACE=${NAMESPACE} | ${SET_NAMESPACE} $NAMESPACE | ${IT_MANIFEST_FILTER} | ${KUBECTL} apply -f -" for filename in obj.files.to_list()]
files.append(ctx.executable._template_engine)
# create namespace script
ctx.actions.expand_template(
template = ctx.file._namespace_template,
substitutions = {
"%{it_sidecar}": ctx.executable._it_sidecar.short_path,
"%{cluster}": ctx.file.cluster.path,
"%{kubeconfig}": ctx.file.kubeconfig.path,
"%{kubectl}": ctx.file.kubectl.path,
"%{portforwards}": " ".join(["-portforward=" + p for p in ctx.attr.portforward_services]),
"%{push_statements}": push_statements,
"%{set_namespace}": ctx.executable._set_namespace.short_path,
"%{it_manifest_filter}": ctx.executable._it_manifest_filter.short_path,
"%{statements}": "\n".join(commands),
"%{test_timeout}": ctx.attr.setup_timeout,
"%{waitforapps}": " ".join(["-waitforapp=" + p for p in ctx.attr.wait_for_apps]),
},
output = ctx.outputs.executable,
)
rf = ctx.runfiles(files = files, transitive_files = depset(transitive = transitive))
rf = rf.merge(ctx.attr._set_namespace[DefaultInfo].default_runfiles)
for dep_rf in pushes_runfiles:
rf = rf.merge(dep_rf)
return [DefaultInfo(
executable = ctx.outputs.executable,
runfiles = rf,
)]
k8s_test_setup = rule(
attrs = {
"kubeconfig": attr.label(
default = Label("@k8s_test//:kubeconfig"),
allow_single_file = True,
),
"kubectl": attr.label(
default = Label("@k8s_test//:kubectl"),
cfg = "exec",
executable = True,
allow_single_file = True,
),
"objects": attr.label_list(
cfg = "target",
),
"portforward_services": attr.string_list(),
"setup_timeout": attr.string(default = "10m"),
"wait_for_apps": attr.string_list(),
"cluster": attr.label(
default = Label("@k8s_test//:cluster"),
allow_single_file = True,
),
"_it_sidecar": attr.label(
default = Label("//testing/it_sidecar:it_sidecar"),
cfg = "exec",
executable = True,
),
"_namespace_template": attr.label(
default = Label("//testing/private:k8s_test_namespace.sh.tpl"),
allow_single_file = True,
),
"_set_namespace": attr.label(
default = Label("//testing/private:set_namespace"),
cfg = "exec",
executable = True,
),
"_it_manifest_filter": attr.label(
default = Label("//testing/it_manifest_filter:it_manifest_filter"),
cfg = "exec",
executable = True,
),
"_stamper": attr.label(
default = Label("//stamper:stamper"),
cfg = "exec",
executable = True,
allow_files = True,
),
"_template_engine": attr.label(
default = Label("//templating:fast_template_engine"),
executable = True,
cfg = "exec",
),
},
executable = True,
implementation = _k8s_test_setup_impl,
toolchains = ["@rules_gitops//kustomize:toolchain_type"],
)
================================================
FILE: testing/private/set_namespace.sh
================================================
#!/usr/bin/env bash
# Copyright 2026 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
set +x
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
if [ "$1" == "" ]; then
echo usage:
echo $0 'namespace out.yaml'
exit 1
fi
set -euo pipefail
dir=$(mktemp -d)
cat >${dir}/in.yaml
cat >${dir}/kustomization.yaml </.heapdump.hprof`.
# You should configure CI to upload this artifact for later inspection.
common --heap_dump_on_oom
# Docs: https://registry.build/flag/bazel?filter=heap_dump_on_oom
# Allow the Bazel server to check directory sources for changes. Ensures that the Bazel server
# notices when a directory changes, if you have a directory listed in the srcs of some target.
# Recommended when using [copy_directory](https://github.com/bazel-contrib/bazel-lib/blob/main/docs/copy_directory.md)
# and [rules_js](https://github.com/aspect-build/rules_js) since npm package are source directories inputs to copy_directory actions.
startup --host_jvm_args="-DBAZEL_TRACK_SOURCE_DIRECTORIES=1"
# Docs: https://registry.build/flag/bazel?filter=host_jvm_args
# By default, Bazel automatically creates __init__.py files for py_binary and py_test targets.
# From https://github.com/bazelbuild/bazel/issues/10076:
# > It is magic at a distance.
# > Python programmers are already used to creating __init__.py files in their source trees,
# > so doing it behind their backs introduces confusion and changes the semantics of imports
common --incompatible_default_to_explicit_init_py
# Docs: https://registry.build/flag/bazel?filter=incompatible_default_to_explicit_init_py
# Disallow empty glob patterns.
# The glob() function tends to be error-prone, because any typo in a path will silently return an empty list.
# This flag was added in Bazel 0.27 and flipped in Bazel 8: https://github.com/bazelbuild/bazel/issues/8195
common --incompatible_disallow_empty_glob
# Docs: https://registry.build/flag/bazel?filter=incompatible_disallow_empty_glob
# Accept multiple --modify_execution_info flags, rather than the last flag overwriting earlier ones.
common --incompatible_modify_execution_info_additive
# Docs: https://registry.build/flag/bazel?filter=incompatible_modify_execution_info_additive
# Make builds more reproducible by using a static value for PATH and not inheriting LD_LIBRARY_PATH.
# Use `--action_env=ENV_VARIABLE` if you want to inherit specific variables from the environment where Bazel runs.
# Note that doing so can prevent cross-user caching if a shared cache is used.
# See https://github.com/bazelbuild/bazel/issues/2574 for more details.
common --incompatible_strict_action_env
# Docs: https://registry.build/flag/bazel?filter=incompatible_strict_action_env
# Performance improvement: avoid laying out a second copy of the runfiles tree.
# See https://github.com/bazelbuild/bazel/issues/23574.
# This flag was flipped for Bazel 8.
common --nolegacy_external_runfiles
# Docs: https://registry.build/flag/bazel?filter=legacy_external_runfiles
# On CI, don't download remote outputs to the local machine.
# Most CI pipelines don't need to access the files and they can remain at rest on the remote cache.
# Significant time can be spent on needless downloads, which is especially noticeable on fully-cached builds.
#
# If you do need to download files, the fastest options are:
# - (preferred) Use `remote_download_regex` to specify the files to download.
# - Use the Remote Output Service (https://blog.bazel.build/2024/07/23/remote-output-service.html)
# to lazy-materialize specific files after the build completes.
# - Perform a second bazel command with specific targets and override this flag with the `toplevel` value.
# - To copy executable targets, you can use `bazel run --run_under=cp //some:binary_target `.
common:ci --remote_download_outputs="minimal"
# Docs: https://registry.build/flag/bazel?filter=remote_download_outputs
# On CI, fall back to standalone local execution strategy if remote execution fails.
# Otherwise, when a grpc remote cache connection fails, it would fail the build.
common:ci --remote_local_fallback
# Docs: https://registry.build/flag/bazel?filter=remote_local_fallback
# On CI, extend the maximum amount of time to wait for remote execution and cache calls.
common:ci --remote_timeout=3600
# Docs: https://registry.build/flag/bazel?filter=remote_timeout
# Do not upload locally executed action results to the remote cache.
# This should be the default for local builds so local builds cannot poison the remote cache.
#
# Note that this flag is flipped to True under --config=ci, see below.
common --noremote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# On CI, upload locally executed action results to the remote cache.
common:ci --remote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# Repository rules, such as rules_jvm_external: put Bazel's JDK on the path.
# Avoids non-hermeticity from dependency on a JAVA_HOME pointing at a system JDK
# see https://github.com/bazelbuild/rules_jvm_external/issues/445
common --repo_env="JAVA_HOME=../bazel_tools/jdk"
# Docs: https://registry.build/flag/bazel?filter=repo_env
# Reuse sandbox directories between invocations.
# Directories used by sandboxed non-worker execution may be reused to avoid unnecessary setup costs.
# Saves time on sandbox creation and deletion when many of the same kind of action is spawned during the build.
common --reuse_sandbox_directories
# Docs: https://registry.build/flag/bazel?filter=reuse_sandbox_directories
# Don't allow network access for build actions in the sandbox by default.
# Avoids accidental non-hermeticity in actions/tests which depend on remote services.
# Developers should tag targets with `tags=["requires-network"]` to be explicit that they need network access.
# Note that the sandbox cannot print a message to the console if it denies network access,
# so failures under this flag appear as application errors in the networking layer.
common --nosandbox_default_allow_network
# Docs: https://registry.build/flag/bazel?filter=sandbox_default_allow_network
# Only show progress every 60 seconds on CI.
# We want to find a compromise between printing often enough to show that the build isn't stuck,
# but not so often that we produce a long log file that requires a lot of scrolling.
common:ci --show_progress_rate_limit=60
# Docs: https://registry.build/flag/bazel?filter=show_progress_rate_limit
# The printed files are convenient strings for copy+pasting to the shell, to execute them.
# This option requires an integer argument, which is the threshold number of targets above which result information is not printed.
# Show the output files created by builds that requested more than one target.
# This helps users locate the build outputs in more cases.
common --show_result=20
# Docs: https://registry.build/flag/bazel?filter=show_result
# On CI, add a timestamp to each message generated by Bazel specifying the time at which the message was displayed.
# This makes it easier to reason about what were the slowest steps on CI.
common:ci --show_timestamps
# Docs: https://registry.build/flag/bazel?filter=show_timestamps
# The terminal width in columns. Configure this to override the default value based on what your CI system renders.
common:ci --terminal_columns=143
# Docs: https://registry.build/flag/bazel?filter=terminal_columns
# Output test errors to stderr so users don't have to `cat` or open test failure log files when test fail.
# This makes the log noisier in exchange for reducing the time-to-feedback on test failures for users.
common --test_output="errors"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Stream stdout/stderr output from each test in real-time.
# This provides immediate feedback during test execution, useful for debugging test failures.
common:debug --test_output="streamed"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Run one test at a time in exclusive mode.
# This prevents test interference and provides clearer output when debugging test issues.
common:debug --test_strategy="exclusive"
# Docs: https://registry.build/flag/bazel?filter=test_strategy
# The default test_summary ("short") prints a result for every test target that was executed.
# In a large repo this amounts to hundreds of lines of additional log output when testing a broad wildcard pattern like //...
# This value means to print information only about unsuccessful tests that were run.
test:ci --test_summary="terse"
# Docs: https://registry.build/flag/bazel?filter=test_summary
# Prevent long running tests from timing out.
# Set to a high value to allow tests to complete even if they take longer than expected.
common:debug --test_timeout=9999
# Docs: https://registry.build/flag/bazel?filter=test_timeout
================================================
FILE: tools/preset8.bazelrc
================================================
# Generated by bazelrc-preset.bzl
# To update this file, run:
# bazel run @@//tools:preset.update
# On CI, announce all announces command options read from the bazelrc file(s) when starting up at the
# beginning of each Bazel invocation. This is very useful on CI to be able to inspect which flags
# are being applied on each run based on the order of overrides.
common:ci --announce_rc
# Docs: https://registry.build/flag/bazel?filter=announce_rc
# Avoid creating a runfiles tree for binaries or tests until it is needed.
# See https://github.com/bazelbuild/bazel/issues/6627
# This may break local workflows that `build` a binary target, then run the resulting program outside of `bazel run`.
# In those cases, the script will need to call `bazel build --build_runfile_links //my/binary:target` and then execute the resulting program.
common --nobuild_runfile_links
# Docs: https://registry.build/flag/bazel?filter=build_runfile_links
# Always run tests even if they have cached results.
# This ensures tests are executed fresh each time, useful for debugging and ensuring test reliability.
common:debug --nocache_test_results
# Docs: https://registry.build/flag/bazel?filter=cache_test_results
# Don’t encourage a rules author to update their deps if not needed.
# These bazel_dep calls should indicate the minimum version constraint of the ruleset.
# If the author instead updates to the newest of any of their transitives, as this flag would suggest,
# then they'll also force their dependents to a newer version.
# Context:
# https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0
common:ruleset --check_direct_dependencies="off"
# Docs: https://registry.build/flag/bazel?filter=check_direct_dependencies
# On CI, use colors to highlight output on the screen. Set to `no` if your CI does not display colors.
common:ci --color="yes"
# Docs: https://registry.build/flag/bazel?filter=color
# On CI, use cursor controls in screen output.
common:ci --curses="yes"
# Docs: https://registry.build/flag/bazel?filter=curses
# Bazel picks up host-OS-specific config lines from bazelrc files. For example, if the host OS is
# Linux and you run bazel build, Bazel picks up lines starting with build:linux. Supported OS
# identifiers are `linux`, `macos`, `windows`, `freebsd`, and `openbsd`. Enabling this flag is
# equivalent to using `--config=linux` on Linux, `--config=windows` on Windows, etc.
common --enable_platform_specific_config
# Docs: https://registry.build/flag/bazel?filter=enable_platform_specific_config
# Speed up all builds by not checking if external repository files have been modified.
# For reference: https://github.com/bazelbuild/bazel/blob/1af61b21df99edc2fc66939cdf14449c2661f873/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java#L244
common --noexperimental_check_external_repository_files
# Docs: https://registry.build/flag/bazel?filter=experimental_check_external_repository_files
# Always download coverage files for tests from the remote cache. By default, coverage files are not
# downloaded on test result cache hits when --remote_download_minimal is enabled, making it impossible
# to generate a full coverage report.
common --experimental_fetch_all_coverage_outputs
# Docs: https://registry.build/flag/bazel?filter=experimental_fetch_all_coverage_outputs
# Set this flag to enable re-tries of failed tests on CI.
# When any test target fails, try one or more times. This applies regardless of whether the "flaky"
# tag appears on the target definition.
# This is a tradeoff: legitimately failing tests will take longer to report,
# but we can "paper over" flaky tests that pass most of the time.
#
# An alternative is to mark every flaky test with the `flaky = True` attribute, but this requires
# the buildcop to make frequent code edits.
# This flag is not recommended for local builds: flakiness is more likely to get fixed if it is
# observed during development.
#
# Note that when passing after the first attempt, Bazel will give a special "FLAKY" status rather than "PASSED".
test:ci --flaky_test_attempts=2
# Docs: https://registry.build/flag/bazel?filter=flaky_test_attempts
# Fixes builds hanging on CI that get the TCP connection closed without sending RST packets.
common:ci --grpc_keepalive_time="30s"
# Docs: https://registry.build/flag/bazel?filter=grpc_keepalive_time
# Output a heap dump if an OOM is thrown during a Bazel invocation
# (including OOMs due to `--experimental_oom_more_eagerly_threshold`).
# The dump will be written to `/.heapdump.hprof`.
# You should configure CI to upload this artifact for later inspection.
common --heap_dump_on_oom
# Docs: https://registry.build/flag/bazel?filter=heap_dump_on_oom
# Allow the Bazel server to check directory sources for changes. Ensures that the Bazel server
# notices when a directory changes, if you have a directory listed in the srcs of some target.
# Recommended when using [copy_directory](https://github.com/bazel-contrib/bazel-lib/blob/main/docs/copy_directory.md)
# and [rules_js](https://github.com/aspect-build/rules_js) since npm package are source directories inputs to copy_directory actions.
startup --host_jvm_args="-DBAZEL_TRACK_SOURCE_DIRECTORIES=1"
# Docs: https://registry.build/flag/bazel?filter=host_jvm_args
# By default, Bazel automatically creates __init__.py files for py_binary and py_test targets.
# From https://github.com/bazelbuild/bazel/issues/10076:
# > It is magic at a distance.
# > Python programmers are already used to creating __init__.py files in their source trees,
# > so doing it behind their backs introduces confusion and changes the semantics of imports
common --incompatible_default_to_explicit_init_py
# Docs: https://registry.build/flag/bazel?filter=incompatible_default_to_explicit_init_py
# Fail if Starlark files are not UTF-8 encoded.
# Introduced in Bazel 8.1, see https://github.com/bazelbuild/bazel/pull/24944
common --incompatible_enforce_starlark_utf8="error"
# Docs: https://registry.build/flag/bazel?filter=incompatible_enforce_starlark_utf8
# Accept multiple --modify_execution_info flags, rather than the last flag overwriting earlier ones.
common --incompatible_modify_execution_info_additive
# Docs: https://registry.build/flag/bazel?filter=incompatible_modify_execution_info_additive
# Make builds more reproducible by using a static value for PATH and not inheriting LD_LIBRARY_PATH.
# Use `--action_env=ENV_VARIABLE` if you want to inherit specific variables from the environment where Bazel runs.
# Note that doing so can prevent cross-user caching if a shared cache is used.
# See https://github.com/bazelbuild/bazel/issues/2574 for more details.
common --incompatible_strict_action_env
# Docs: https://registry.build/flag/bazel?filter=incompatible_strict_action_env
# Add the CloudFlare mirror of BCR-referenced downloads.
# Improves reliability of Bazel when CDNs are flaky, for example issues with ftp.gnu.org in 2025.
common --module_mirrors="https://bcr.cloudflaremirrors.com"
# Docs: https://registry.build/flag/bazel?filter=module_mirrors
# On CI, don't download remote outputs to the local machine.
# Most CI pipelines don't need to access the files and they can remain at rest on the remote cache.
# Significant time can be spent on needless downloads, which is especially noticeable on fully-cached builds.
#
# If you do need to download files, the fastest options are:
# - (preferred) Use `remote_download_regex` to specify the files to download.
# - Use the Remote Output Service (https://blog.bazel.build/2024/07/23/remote-output-service.html)
# to lazy-materialize specific files after the build completes.
# - Perform a second bazel command with specific targets and override this flag with the `toplevel` value.
# - To copy executable targets, you can use `bazel run --run_under=cp //some:binary_target `.
common:ci --remote_download_outputs="minimal"
# Docs: https://registry.build/flag/bazel?filter=remote_download_outputs
# On CI, fall back to standalone local execution strategy if remote execution fails.
# Otherwise, when a grpc remote cache connection fails, it would fail the build.
common:ci --remote_local_fallback
# Docs: https://registry.build/flag/bazel?filter=remote_local_fallback
# On CI, extend the maximum amount of time to wait for remote execution and cache calls.
common:ci --remote_timeout=3600
# Docs: https://registry.build/flag/bazel?filter=remote_timeout
# Do not upload locally executed action results to the remote cache.
# This should be the default for local builds so local builds cannot poison the remote cache.
#
# Note that this flag is flipped to True under --config=ci, see below.
common --noremote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# On CI, upload locally executed action results to the remote cache.
common:ci --remote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# Repository rules, such as rules_jvm_external: put Bazel's JDK on the path.
# Avoids non-hermeticity from dependency on a JAVA_HOME pointing at a system JDK
# see https://github.com/bazelbuild/rules_jvm_external/issues/445
common --repo_env="JAVA_HOME=../bazel_tools/jdk"
# Docs: https://registry.build/flag/bazel?filter=repo_env
# Reuse sandbox directories between invocations.
# Directories used by sandboxed non-worker execution may be reused to avoid unnecessary setup costs.
# Saves time on sandbox creation and deletion when many of the same kind of action is spawned during the build.
common --reuse_sandbox_directories
# Docs: https://registry.build/flag/bazel?filter=reuse_sandbox_directories
# Don't allow network access for build actions in the sandbox by default.
# Avoids accidental non-hermeticity in actions/tests which depend on remote services.
# Developers should tag targets with `tags=["requires-network"]` to be explicit that they need network access.
# Note that the sandbox cannot print a message to the console if it denies network access,
# so failures under this flag appear as application errors in the networking layer.
common --nosandbox_default_allow_network
# Docs: https://registry.build/flag/bazel?filter=sandbox_default_allow_network
# Only show progress every 60 seconds on CI.
# We want to find a compromise between printing often enough to show that the build isn't stuck,
# but not so often that we produce a long log file that requires a lot of scrolling.
common:ci --show_progress_rate_limit=60
# Docs: https://registry.build/flag/bazel?filter=show_progress_rate_limit
# The printed files are convenient strings for copy+pasting to the shell, to execute them.
# This option requires an integer argument, which is the threshold number of targets above which result information is not printed.
# Show the output files created by builds that requested more than one target.
# This helps users locate the build outputs in more cases.
common --show_result=20
# Docs: https://registry.build/flag/bazel?filter=show_result
# On CI, add a timestamp to each message generated by Bazel specifying the time at which the message was displayed.
# This makes it easier to reason about what were the slowest steps on CI.
common:ci --show_timestamps
# Docs: https://registry.build/flag/bazel?filter=show_timestamps
# The terminal width in columns. Configure this to override the default value based on what your CI system renders.
common:ci --terminal_columns=143
# Docs: https://registry.build/flag/bazel?filter=terminal_columns
# Output test errors to stderr so users don't have to `cat` or open test failure log files when test fail.
# This makes the log noisier in exchange for reducing the time-to-feedback on test failures for users.
common --test_output="errors"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Stream stdout/stderr output from each test in real-time.
# This provides immediate feedback during test execution, useful for debugging test failures.
common:debug --test_output="streamed"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Run one test at a time in exclusive mode.
# This prevents test interference and provides clearer output when debugging test issues.
common:debug --test_strategy="exclusive"
# Docs: https://registry.build/flag/bazel?filter=test_strategy
# The default test_summary ("short") prints a result for every test target that was executed.
# In a large repo this amounts to hundreds of lines of additional log output when testing a broad wildcard pattern like //...
# This value means to print information only about unsuccessful tests that were run.
test:ci --test_summary="terse"
# Docs: https://registry.build/flag/bazel?filter=test_summary
# Prevent long running tests from timing out.
# Set to a high value to allow tests to complete even if they take longer than expected.
common:debug --test_timeout=9999
# Docs: https://registry.build/flag/bazel?filter=test_timeout
================================================
FILE: tools/preset9.bazelrc
================================================
# Generated by bazelrc-preset.bzl
# To update this file, run:
# bazel run @@//tools:preset.update
# On CI, announce all announces command options read from the bazelrc file(s) when starting up at the
# beginning of each Bazel invocation. This is very useful on CI to be able to inspect which flags
# are being applied on each run based on the order of overrides.
common:ci --announce_rc
# Docs: https://registry.build/flag/bazel?filter=announce_rc
# Avoid creating a runfiles tree for binaries or tests until it is needed.
# See https://github.com/bazelbuild/bazel/issues/6627
# This may break local workflows that `build` a binary target, then run the resulting program outside of `bazel run`.
# In those cases, the script will need to call `bazel build --build_runfile_links //my/binary:target` and then execute the resulting program.
common --nobuild_runfile_links
# Docs: https://registry.build/flag/bazel?filter=build_runfile_links
# Always run tests even if they have cached results.
# This ensures tests are executed fresh each time, useful for debugging and ensuring test reliability.
common:debug --nocache_test_results
# Docs: https://registry.build/flag/bazel?filter=cache_test_results
# Don’t encourage a rules author to update their deps if not needed.
# These bazel_dep calls should indicate the minimum version constraint of the ruleset.
# If the author instead updates to the newest of any of their transitives, as this flag would suggest,
# then they'll also force their dependents to a newer version.
# Context:
# https://bazelbuild.slack.com/archives/C014RARENH0/p1691158021917459?thread_ts=1691156601.420349&cid=C014RARENH0
common:ruleset --check_direct_dependencies="off"
# Docs: https://registry.build/flag/bazel?filter=check_direct_dependencies
# On CI, use colors to highlight output on the screen. Set to `no` if your CI does not display colors.
common:ci --color="yes"
# Docs: https://registry.build/flag/bazel?filter=color
# On CI, use cursor controls in screen output.
common:ci --curses="yes"
# Docs: https://registry.build/flag/bazel?filter=curses
# Bazel picks up host-OS-specific config lines from bazelrc files. For example, if the host OS is
# Linux and you run bazel build, Bazel picks up lines starting with build:linux. Supported OS
# identifiers are `linux`, `macos`, `windows`, `freebsd`, and `openbsd`. Enabling this flag is
# equivalent to using `--config=linux` on Linux, `--config=windows` on Windows, etc.
common --enable_platform_specific_config
# Docs: https://registry.build/flag/bazel?filter=enable_platform_specific_config
# Speed up all builds by not checking if external repository files have been modified.
# For reference: https://github.com/bazelbuild/bazel/blob/1af61b21df99edc2fc66939cdf14449c2661f873/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java#L244
common --noexperimental_check_external_repository_files
# Docs: https://registry.build/flag/bazel?filter=experimental_check_external_repository_files
# Always download coverage files for tests from the remote cache. By default, coverage files are not
# downloaded on test result cache hits when --remote_download_minimal is enabled, making it impossible
# to generate a full coverage report.
common --experimental_fetch_all_coverage_outputs
# Docs: https://registry.build/flag/bazel?filter=experimental_fetch_all_coverage_outputs
# Set this flag to enable re-tries of failed tests on CI.
# When any test target fails, try one or more times. This applies regardless of whether the "flaky"
# tag appears on the target definition.
# This is a tradeoff: legitimately failing tests will take longer to report,
# but we can "paper over" flaky tests that pass most of the time.
#
# An alternative is to mark every flaky test with the `flaky = True` attribute, but this requires
# the buildcop to make frequent code edits.
# This flag is not recommended for local builds: flakiness is more likely to get fixed if it is
# observed during development.
#
# Note that when passing after the first attempt, Bazel will give a special "FLAKY" status rather than "PASSED".
test:ci --flaky_test_attempts=2
# Docs: https://registry.build/flag/bazel?filter=flaky_test_attempts
# Fixes builds hanging on CI that get the TCP connection closed without sending RST packets.
common:ci --grpc_keepalive_time="30s"
# Docs: https://registry.build/flag/bazel?filter=grpc_keepalive_time
# Output a heap dump if an OOM is thrown during a Bazel invocation
# (including OOMs due to `--experimental_oom_more_eagerly_threshold`).
# The dump will be written to `/.heapdump.hprof`.
# You should configure CI to upload this artifact for later inspection.
common --heap_dump_on_oom
# Docs: https://registry.build/flag/bazel?filter=heap_dump_on_oom
# Allow the Bazel server to check directory sources for changes. Ensures that the Bazel server
# notices when a directory changes, if you have a directory listed in the srcs of some target.
# Recommended when using [copy_directory](https://github.com/bazel-contrib/bazel-lib/blob/main/docs/copy_directory.md)
# and [rules_js](https://github.com/aspect-build/rules_js) since npm package are source directories inputs to copy_directory actions.
startup --host_jvm_args="-DBAZEL_TRACK_SOURCE_DIRECTORIES=1"
# Docs: https://registry.build/flag/bazel?filter=host_jvm_args
# By default, Bazel automatically creates __init__.py files for py_binary and py_test targets.
# From https://github.com/bazelbuild/bazel/issues/10076:
# > It is magic at a distance.
# > Python programmers are already used to creating __init__.py files in their source trees,
# > so doing it behind their backs introduces confusion and changes the semantics of imports
common --incompatible_default_to_explicit_init_py
# Docs: https://registry.build/flag/bazel?filter=incompatible_default_to_explicit_init_py
# Fail if Starlark files are not UTF-8 encoded.
# Introduced in Bazel 8.1, see https://github.com/bazelbuild/bazel/pull/24944
common --incompatible_enforce_starlark_utf8="error"
# Docs: https://registry.build/flag/bazel?filter=incompatible_enforce_starlark_utf8
# Accept multiple --modify_execution_info flags, rather than the last flag overwriting earlier ones.
common --incompatible_modify_execution_info_additive
# Docs: https://registry.build/flag/bazel?filter=incompatible_modify_execution_info_additive
# Make builds more reproducible by using a static value for PATH and not inheriting LD_LIBRARY_PATH.
# Use `--action_env=ENV_VARIABLE` if you want to inherit specific variables from the environment where Bazel runs.
# Note that doing so can prevent cross-user caching if a shared cache is used.
# See https://github.com/bazelbuild/bazel/issues/2574 for more details.
common --incompatible_strict_action_env
# Docs: https://registry.build/flag/bazel?filter=incompatible_strict_action_env
# Add the CloudFlare mirror of BCR-referenced downloads.
# Improves reliability of Bazel when CDNs are flaky, for example issues with ftp.gnu.org in 2025.
common --module_mirrors="https://bcr.cloudflaremirrors.com"
# Docs: https://registry.build/flag/bazel?filter=module_mirrors
# On CI, don't download remote outputs to the local machine.
# Most CI pipelines don't need to access the files and they can remain at rest on the remote cache.
# Significant time can be spent on needless downloads, which is especially noticeable on fully-cached builds.
#
# If you do need to download files, the fastest options are:
# - (preferred) Use `remote_download_regex` to specify the files to download.
# - Use the Remote Output Service (https://blog.bazel.build/2024/07/23/remote-output-service.html)
# to lazy-materialize specific files after the build completes.
# - Perform a second bazel command with specific targets and override this flag with the `toplevel` value.
# - To copy executable targets, you can use `bazel run --run_under=cp //some:binary_target `.
common:ci --remote_download_outputs="minimal"
# Docs: https://registry.build/flag/bazel?filter=remote_download_outputs
# On CI, fall back to standalone local execution strategy if remote execution fails.
# Otherwise, when a grpc remote cache connection fails, it would fail the build.
common:ci --remote_local_fallback
# Docs: https://registry.build/flag/bazel?filter=remote_local_fallback
# On CI, extend the maximum amount of time to wait for remote execution and cache calls.
common:ci --remote_timeout=3600
# Docs: https://registry.build/flag/bazel?filter=remote_timeout
# Do not upload locally executed action results to the remote cache.
# This should be the default for local builds so local builds cannot poison the remote cache.
#
# Note that this flag is flipped to True under --config=ci, see below.
common --noremote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# On CI, upload locally executed action results to the remote cache.
common:ci --remote_upload_local_results
# Docs: https://registry.build/flag/bazel?filter=remote_upload_local_results
# Repository rules, such as rules_jvm_external: put Bazel's JDK on the path.
# Avoids non-hermeticity from dependency on a JAVA_HOME pointing at a system JDK
# see https://github.com/bazelbuild/rules_jvm_external/issues/445
common --repo_env="JAVA_HOME=../bazel_tools/jdk"
# Docs: https://registry.build/flag/bazel?filter=repo_env
# Reuse sandbox directories between invocations.
# Directories used by sandboxed non-worker execution may be reused to avoid unnecessary setup costs.
# Saves time on sandbox creation and deletion when many of the same kind of action is spawned during the build.
common --reuse_sandbox_directories
# Docs: https://registry.build/flag/bazel?filter=reuse_sandbox_directories
# Don't allow network access for build actions in the sandbox by default.
# Avoids accidental non-hermeticity in actions/tests which depend on remote services.
# Developers should tag targets with `tags=["requires-network"]` to be explicit that they need network access.
# Note that the sandbox cannot print a message to the console if it denies network access,
# so failures under this flag appear as application errors in the networking layer.
common --nosandbox_default_allow_network
# Docs: https://registry.build/flag/bazel?filter=sandbox_default_allow_network
# Only show progress every 60 seconds on CI.
# We want to find a compromise between printing often enough to show that the build isn't stuck,
# but not so often that we produce a long log file that requires a lot of scrolling.
common:ci --show_progress_rate_limit=60
# Docs: https://registry.build/flag/bazel?filter=show_progress_rate_limit
# The printed files are convenient strings for copy+pasting to the shell, to execute them.
# This option requires an integer argument, which is the threshold number of targets above which result information is not printed.
# Show the output files created by builds that requested more than one target.
# This helps users locate the build outputs in more cases.
common --show_result=20
# Docs: https://registry.build/flag/bazel?filter=show_result
# On CI, add a timestamp to each message generated by Bazel specifying the time at which the message was displayed.
# This makes it easier to reason about what were the slowest steps on CI.
common:ci --show_timestamps
# Docs: https://registry.build/flag/bazel?filter=show_timestamps
# The terminal width in columns. Configure this to override the default value based on what your CI system renders.
common:ci --terminal_columns=143
# Docs: https://registry.build/flag/bazel?filter=terminal_columns
# Output test errors to stderr so users don't have to `cat` or open test failure log files when test fail.
# This makes the log noisier in exchange for reducing the time-to-feedback on test failures for users.
common --test_output="errors"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Stream stdout/stderr output from each test in real-time.
# This provides immediate feedback during test execution, useful for debugging test failures.
common:debug --test_output="streamed"
# Docs: https://registry.build/flag/bazel?filter=test_output
# Run one test at a time in exclusive mode.
# This prevents test interference and provides clearer output when debugging test issues.
common:debug --test_strategy="exclusive"
# Docs: https://registry.build/flag/bazel?filter=test_strategy
# The default test_summary ("short") prints a result for every test target that was executed.
# In a large repo this amounts to hundreds of lines of additional log output when testing a broad wildcard pattern like //...
# This value means to print information only about unsuccessful tests that were run.
test:ci --test_summary="terse"
# Docs: https://registry.build/flag/bazel?filter=test_summary
# Prevent long running tests from timing out.
# Set to a high value to allow tests to complete even if they take longer than expected.
common:debug --test_timeout=9999
# Docs: https://registry.build/flag/bazel?filter=test_timeout
================================================
FILE: tools/util.bzl
================================================
"""Utility functions for golden testing."""
load("@bazel_lib//lib:write_source_files.bzl", "write_source_file")
def golden_test(name, in_file, extension = ""):
write_source_file(
name = name,
in_file = in_file,
out_file = "goldens/{}.golden{}".format(in_file, extension),
diff_test = True,
testonly = True,
)