From 42d7c3974eb1ecf196d7398eabb00f87fef47f7f Mon Sep 17 00:00:00 2001 From: Aleksey Pesternikov Date: Thu, 2 Jul 2026 09:23:15 -0700 Subject: [PATCH 1/4] initial implementation --- README.md | 17 + gitops/BUILD.bazel | 5 +- gitops/cloudrun.bzl | 532 ++++++++++++++++++++++++++ gitops/cloudrun.sh.tpl | 38 ++ gitops/defs.bzl | 2 + gitops/prer/prer_test.sh | 2 + gitops/testing/BUILD.bazel | 69 +++- gitops/testing/cloudrun_apply_test.sh | 95 +++++ skylib/tests/cloudrun/BUILD.bazel | 11 +- 9 files changed, 765 insertions(+), 6 deletions(-) create mode 100644 gitops/cloudrun.bzl create mode 100644 gitops/cloudrun.sh.tpl create mode 100755 gitops/testing/cloudrun_apply_test.sh diff --git a/README.md b/README.md index c93e1356..f9f29dfe 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Bazel GitOps Rules is an alternative to [rules_k8s](https://github.com/bazelbuil ## Rules * [k8s_deploy](#k8s_deploy) +* [cloudrun_deploy](#cloudrun_deploy) * [k8s_test_setup](#k8s_test_setup) @@ -96,6 +97,22 @@ When you run `bazel run ///helloworld:mynamespace.apply`, it applies this file i | ***visibility*** | [Default_visibility](https://docs.bazel.build/versions/master/be/functions.html#package.default_visibility) | Changes the visibility of all rules generated by this macro. See [Bazel docs on visibility](https://docs.bazel.build/versions/master/be/common-definitions.html#common-attributes). + +## cloudrun_deploy + +The `cloudrun_deploy` macro creates targets for deploying Knative Services to Google Cloud Run. It is defined in [cloudrun.bzl](./gitops/cloudrun.bzl) and is loaded via `load("@rules_gitops//gitops:defs.bzl", "cloudrun_deploy")`. + +Similar to `k8s_deploy`, it uses kustomize to render the Knative Service manifest. +When you run a `.apply` target, it deploys the service by running `gcloud run services replace`. +When you run a `.delete` target, it deletes the service by running `gcloud run services delete`. +When you run a `.gitops` target, it copies the rendered manifest to the location `cloud/{project}/{region}/{name}.yaml` inside the deployment repository. + +It accepts similar parameters to `k8s_deploy`, with the following differences: +- ***project*** (instead of `namespace`): The GCP project ID. This is also automatically set as the `namespace` field in the Knative Service YAML. +- ***region*** (instead of `cluster`): The GCP region (e.g. `us-central1`). +- ***service***: The name of the Cloud Run service. + + ### Base Manifests and Overlays diff --git a/gitops/BUILD.bazel b/gitops/BUILD.bazel index 43a7eec5..65d86d21 100644 --- a/gitops/BUILD.bazel +++ b/gitops/BUILD.bazel @@ -12,7 +12,10 @@ load("//gitops/private:kustomize_toolchain.bzl", "current_kustomize_toolchain") package(default_visibility = ["//visibility:public"]) -exports_files(["create_gitops_prs.tpl.sh"]) +exports_files([ + "create_gitops_prs.tpl.sh", + "cloudrun.sh.tpl", +]) toolchain_type( name = "kustomize_toolchain_type", diff --git a/gitops/cloudrun.bzl b/gitops/cloudrun.bzl new file mode 100644 index 00000000..cc278297 --- /dev/null +++ b/gitops/cloudrun.bzl @@ -0,0 +1,532 @@ +# Copyright 2026 The rules_gitops Authors. +# +# 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. + +load("//gitops:provider.bzl", "GitopsArtifactsInfo", "GitopsPushInfo") +load("//push_oci:push_oci.bzl", "push_oci") +load("//skylib:push_alias.bzl", "pushed_image_alias") +load("//skylib:runfile.bzl", "get_runfile_path") +load("//skylib:stamp.bzl", "stamp") +load( + "//skylib/kustomize:kustomize.bzl", + "imagePushStatements", + "kustomize", +) + +def _image_pushes(name_suffix, images, image_registry, image_repository, image_digest_tag, tags = []): + image_pushes = [] + + def process_image(image_label, image_alias = None): + rule_name_parts = [image_label, image_registry, image_repository] + rule_name_parts = [p for p in rule_name_parts if p] + rule_name = "_".join(rule_name_parts) + rule_name = rule_name.replace("/", "_").replace(":", "_").replace("@", "_").replace(".", "_") + rule_name = rule_name.strip("_") + if not native.existing_rule(rule_name + name_suffix): + push_oci( + name = rule_name + name_suffix, + image = image_label, # buildifier: disable=uninitialized + image_digest_tag = image_digest_tag, + registry = image_registry, + repository = image_repository, + tags = tags, + visibility = ["//visibility:public"], + ) + if not image_alias: + return rule_name + name_suffix + + if not native.existing_rule(rule_name + "_alias_" + name_suffix): + pushed_image_alias( + name = rule_name + "_alias_" + name_suffix, + alias = image_alias, + pushed_image = rule_name + name_suffix, + tags = tags, + visibility = ["//visibility:public"], + ) + return rule_name + "_alias_" + name_suffix + + if type(images) == "dict": + for image_alias in images: + image = images[image_alias] + push = process_image(image, image_alias) + image_pushes.append(push) + else: + for image in images: + push = process_image(image) + image_pushes.append(push) + return image_pushes + +def _gcloud_run_impl(ctx): + files = [] + ctx.files.srcs + statements = "" + transitive = None + transitive_runfiles = [] + + project = ctx.attr.project + if "{" in ctx.attr.project: + project = stamp(ctx, project, files, ctx.label.name + ".project-name", True) + + region = ctx.attr.region + if "{" in ctx.attr.region: + region = stamp(ctx, region, files, ctx.label.name + ".region-name", True) + + service = ctx.attr.service + if "{" in ctx.attr.service: + service = stamp(ctx, service, files, ctx.label.name + ".service-name", True) + + command = ctx.attr.command + push = ctx.attr.push + + files += [ctx.executable._template_engine, ctx.file._info_file] + + if push: + pushes = [obj[GitopsArtifactsInfo].image_pushes for obj in ctx.attr.srcs] + trans_img_pushes = depset(transitive = pushes).to_list() + trans_img_pushes = [push for push in trans_img_pushes if push.files_to_run.executable] + statements += "\n".join([ + "# {}\n".format(exe[GitopsPushInfo].image_label) + + "echo pushing {}".format(exe[GitopsPushInfo].repository if GitopsPushInfo in exe else "") + for exe in trans_img_pushes + ]) + "\n" + statements += "\n".join([ + "async \"%s\"" % get_runfile_path(ctx, exe.files_to_run.executable) + for exe in trans_img_pushes + ]) + "\nwaitpids\n" + files += [obj.files_to_run.executable for obj in trans_img_pushes] + transitive = depset(transitive = [obj.default_runfiles.files for obj in trans_img_pushes]) + transitive_runfiles += [exe[DefaultInfo].default_runfiles for exe in trans_img_pushes] + + for inattr in ctx.attr.srcs: + for infile in inattr.files.to_list(): + if command == "replace": + statements += "{template_engine} --template={infile} --variable=PROJECT=\"$PROJECT\" --variable=REGION=\"$REGION\" --variable=SERVICE=\"$SERVICE\" --stamp_info_file={info_file} | gcloud run services replace - --project=\"$PROJECT\" --region=\"$REGION\"\n".format( + infile = get_runfile_path(ctx, infile), + template_engine = get_runfile_path(ctx, ctx.executable._template_engine), + info_file = get_runfile_path(ctx, ctx.file._info_file), + ) + elif command == "delete": + statements += "gcloud run services delete \"$SERVICE\" --project=\"$PROJECT\" --region=\"$REGION\" --quiet\n" + else: + fail("Unsupported command: %s" % command) + + ctx.actions.expand_template( + template = ctx.file._template, + substitutions = { + "%{project}": project, + "%{region}": region, + "%{service}": service, + "%{statements}": statements, + }, + output = ctx.outputs.executable, + ) + + runfiles = ctx.runfiles(files = files, transitive_files = transitive) + runfiles = runfiles.merge_all(transitive_runfiles) + + return [ + DefaultInfo(runfiles = runfiles), + ] + +gcloud_run = rule( + attrs = { + "srcs": attr.label_list(providers = (GitopsArtifactsInfo,)), + "project": attr.string(mandatory = True), + "region": attr.string(mandatory = True), + "service": attr.string(mandatory = True), + "command": attr.string(default = "replace"), + "push": attr.bool(default = True), + "_build_user_value": attr.label( + default = Label("//skylib:build_user_value.txt"), + allow_single_file = True, + ), + "_info_file": attr.label( + default = Label("//skylib: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("//gitops:cloudrun.sh.tpl"), + allow_single_file = True, + ), + "_template_engine": attr.label( + default = Label("//templating:fast_template_engine"), + executable = True, + cfg = "exec", + ), + }, + executable = True, + implementation = _gcloud_run_impl, +) + +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 _cloudrun_gitops_impl(ctx): + region = ctx.attr.region + service = ctx.attr.service + project = ctx.attr.project + strip_prefixes = ctx.attr.strip_prefixes + files = [] + + push_statements, files, pushes_runfiles = imagePushStatements(ctx, ctx.attr.srcs, files) + statements = """if [ "$PERFORM_PUSH" == "1" ]; then +{} +fi + """.format(push_statements) + + if "{" in service: + fail("unable to gitops service with placeholders %s" % service) + if "{" in region: + fail("unable to gitops region with placeholders %s" % region) + if "{" in project: + fail("unable to gitops project with placeholders %s" % project) + + for inattr in ctx.attr.srcs: + for infile in inattr.files.to_list(): + statements += ("echo $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n" + + "mkdir -p $TARGET_DIR/{gitops_path}/{project}/{region}\n" + + "echo '# GENERATED BY {rulename} -> {gitopsrulename}' > $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n" + + "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --variable=SERVICE={service} --stamp_info_file={info_file} >> $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n").format( + infile = get_runfile_path(ctx, infile), + rulename = inattr.label, + gitopsrulename = ctx.label, + project = project, + gitops_path = ctx.attr.gitops_path, + region = region, + service = service, + file = _remove_prefixes(infile.path.split("/")[-1], strip_prefixes), + template_engine = get_runfile_path(ctx, ctx.executable._template_engine), + info_file = get_runfile_path(ctx, ctx.file._info_file), + ) + + ctx.actions.expand_template( + template = ctx.file._template, + substitutions = { + "%{deployment_branch}": ctx.attr.deployment_branch, + "%{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) + for dep_rf in pushes_runfiles: + rf = rf.merge(dep_rf) + return [ + DefaultInfo(runfiles = rf), + GitopsArtifactsInfo( + image_pushes = depset(transitive = [obj[GitopsArtifactsInfo].image_pushes for obj in ctx.attr.srcs]), + deployment_branch = ctx.attr.deployment_branch, + ), + ] + +cloudrun_gitops = rule( + attrs = { + "srcs": attr.label_list(providers = (GitopsArtifactsInfo,)), + "project": attr.string(mandatory = True), + "region": attr.string(mandatory = True), + "service": 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("//skylib: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("//skylib:k8s_gitops.sh.tpl"), + allow_single_file = True, + ), + }, + executable = True, + implementation = _cloudrun_gitops_impl, +) + +def _cloudrun_show_impl(ctx): + script_content = """#!/usr/bin/env bash +set -e + +function guess_runfiles() { + if [ -d "${BASH_SOURCE[0]}.runfiles" ]; then + # Runfiles are adjacent to the current script. + echo "$( cd "${BASH_SOURCE[0]}.runfiles" && pwd )" + else + # The current script is within some other script's runfiles. + mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + echo $mydir | sed -e 's|\\(.*\\.runfiles\\)/.*|\\1|' + fi +} + +RUNFILES=${RUNFILES:-$(guess_runfiles)} +""" + + outputs = [] + script_template = "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --variable=SERVICE={service} --stamp_info_file={info_file}\n" + for dep in ctx.attr.src.files.to_list(): + outputs.append(script_template.format( + infile = get_runfile_path(ctx, dep), + template_engine = get_runfile_path(ctx, ctx.executable._template_engine), + project = ctx.attr.project, + region = ctx.attr.region, + service = ctx.attr.service, + info_file = get_runfile_path(ctx, ctx.file._info_file), + )) + + script_content += "echo '---'\n".join(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)), + ] + +cloudrun_show = rule( + implementation = _cloudrun_show_impl, + attrs = { + "src": attr.label( + doc = "Input file.", + mandatory = True, + ), + "project": attr.string( + mandatory = True, + ), + "region": attr.string( + mandatory = True, + ), + "service": attr.string( + mandatory = True, + ), + "_info_file": attr.label( + default = Label("//skylib:more_stable_status.txt"), + allow_single_file = True, + ), + "_template_engine": attr.label( + default = Label("//templating:fast_template_engine"), + executable = True, + cfg = "exec", + ), + }, + executable = True, +) + +def cloudrun_deploy( + name, + project = None, + region = None, + service = None, + configmaps_srcs = None, + secrets_srcs = None, + configmaps_renaming = None, + manifests = None, + name_prefix = None, + name_suffix = None, + patches = None, + image_name_patches = {}, + image_tag_patches = {}, + substitutions = {}, + configurations = [], + common_labels = {}, + common_annotations = {}, + openapi_path = "@rules_gitops//skylib:run_schema.json", + deps = [], + deps_aliases = {}, + images = [], + image_digest_tag = False, + image_registry = "docker.io", + image_repository = None, + gitops = True, + gitops_path = "cloud", + deployment_branch = None, + release_branch_prefix = "main", + start_tag = "{{", + end_tag = "}}", + tags = [], + visibility = None): + if not manifests: + manifests = native.glob(["*.yaml", "*.yaml.tpl"]) + + for reservedname in ["PROJECT", "REGION", "SERVICE"]: + if substitutions.get(reservedname): + fail("do not put %s in substitutions parameter of cloudrun_deploy. It will be added autimatically" % reservedname) + substitutions = dict(substitutions) + substitutions["PROJECT"] = project + substitutions["REGION"] = region + substitutions["SERVICE"] = service + + if not gitops: + image_pushes = _image_pushes( + name_suffix = "-mynamespace.push", + images = images, + image_registry = image_registry + "/mynamespace", + image_repository = image_repository, + image_digest_tag = image_digest_tag, + tags = tags, + ) + kustomize( + name = name, + namespace = project, + configmaps_srcs = configmaps_srcs, + secrets_srcs = secrets_srcs, + disable_name_suffix_hash = (configmaps_renaming != "hash"), + images = image_pushes, + 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, + image_name_patches = image_name_patches, + image_tag_patches = image_tag_patches, + openapi_path = openapi_path, + tags = tags, + visibility = visibility, + ) + gcloud_run( + name = name + ".apply", + srcs = [name], + project = project, + region = region, + service = service, + command = "replace", + tags = tags, + visibility = visibility, + ) + gcloud_run( + name = name + ".delete", + srcs = [name], + command = "delete", + project = project, + region = region, + service = service, + push = False, + tags = tags, + visibility = visibility, + ) + cloudrun_show( + name = name + ".show", + src = name, + project = project, + region = region, + service = service, + tags = tags, + visibility = visibility, + ) + else: + if not service: + fail("service must be defined for gitops cloudrun_deploy") + if not region: + fail("region must be defined for gitops cloudrun_deploy") + if not project: + fail("project must be defined for gitops cloudrun_deploy") + + image_pushes = _image_pushes( + name_suffix = ".push", + images = images, + image_registry = image_registry, + image_repository = image_repository, + image_digest_tag = image_digest_tag, + tags = tags, + ) + kustomize( + name = name, + namespace = project, + configmaps_srcs = configmaps_srcs, + secrets_srcs = secrets_srcs, + disable_name_suffix_hash = (configmaps_renaming != "hash"), + images = image_pushes, + 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, + openapi_path = openapi_path, + tags = tags, + ) + gcloud_run( + name = name + ".apply", + srcs = [name], + project = project, + region = region, + service = service, + command = "replace", + tags = tags, + visibility = visibility, + ) + gcloud_run( + name = name + ".delete", + srcs = [name], + command = "delete", + project = project, + region = region, + service = service, + push = False, + tags = tags, + visibility = visibility, + ) + cloudrun_gitops( + name = name + ".gitops", + srcs = [name], + project = project, + region = region, + service = service, + gitops_path = gitops_path, + strip_prefixes = [ + service + "-", + region + "-", + ], + deployment_branch = deployment_branch, + release_branch_prefix = release_branch_prefix, + tags = tags, + visibility = ["//visibility:public"], + ) + cloudrun_show( + name = name + ".show", + src = name, + project = project, + region = region, + service = service, + tags = tags, + visibility = visibility, + ) diff --git a/gitops/cloudrun.sh.tpl b/gitops/cloudrun.sh.tpl new file mode 100644 index 00000000..bd34346a --- /dev/null +++ b/gitops/cloudrun.sh.tpl @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright 2026 The rules_gitops Authors. +set -eu + +function guess_runfiles() { + if [ -d ${BASH_SOURCE[0]}.runfiles ]; then + # Runfiles are adjacent to the current script. + echo "$( cd ${BASH_SOURCE[0]}.runfiles && pwd )" + else + # The current script is within some other script's runfiles. + mydir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + echo $mydir | sed -e 's|\(.*\.runfiles\)/.*|\1|' + fi +} + +RUNFILES="${PYTHON_RUNFILES:-$(guess_runfiles)}" + +PIDS=() +function async() { + # Launch the command asynchronously and track its process id. + PYTHON_RUNFILES=${RUNFILES} RUNFILES_DIR=${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 +} + +PROJECT=%{project} +REGION=%{region} +SERVICE=%{service} + +%{statements} diff --git a/gitops/defs.bzl b/gitops/defs.bzl index 3bbf0ff8..e32ef7d9 100644 --- a/gitops/defs.bzl +++ b/gitops/defs.bzl @@ -14,7 +14,9 @@ GitOps rules public interface load("@rules_gitops//skylib:external_image.bzl", _external_iamge = "external_image") load("@rules_gitops//skylib:k8s.bzl", _k8s_deploy = "k8s_deploy", _k8s_test_setup = "k8s_test_setup") +load("@rules_gitops//gitops:cloudrun.bzl", _cloudrun_deploy = "cloudrun_deploy") k8s_deploy = _k8s_deploy k8s_test_setup = _k8s_test_setup external_image = _external_iamge +cloudrun_deploy = _cloudrun_deploy diff --git a/gitops/prer/prer_test.sh b/gitops/prer/prer_test.sh index e5f26a6f..e350bf05 100755 --- a/gitops/prer/prer_test.sh +++ b/gitops/prer/prer_test.sh @@ -51,6 +51,7 @@ EXPECTED_TARGETS=( "//gitops/testing:legacy_alias.gitops" "//gitops/testing:legacy_label.gitops" "//gitops/testing:legacy_renamed_alias.gitops" + "//gitops/testing:prer_cloudrun_target.gitops" ) # Define expected push binaries (relative paths under workspace bazel-out/.../bin) @@ -62,6 +63,7 @@ EXPECTED_PUSH_BINARIES=( "gitops/testing/skylib_kustomize_tests_img_image_docker_io.push" "gitops/testing/img_pushed_image" "gitops/testing/img_pushed_image_docker_io.push" + "gitops/testing/push_apply_cloudrun_pushed_image.sh" ) echo "Verifying gitops targets..." diff --git a/gitops/testing/BUILD.bazel b/gitops/testing/BUILD.bazel index cfa50298..8b9b79d4 100644 --- a/gitops/testing/BUILD.bazel +++ b/gitops/testing/BUILD.bazel @@ -11,7 +11,7 @@ load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files") load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@rules_shell//shell:sh_test.bzl", "sh_test") -load("//gitops:defs.bzl", "external_image", "k8s_deploy", "k8s_test_setup") +load("//gitops:defs.bzl", "cloudrun_deploy", "external_image", "k8s_deploy", "k8s_test_setup") load("//push_oci:push_oci.bzl", "push_oci") licenses(["notice"]) # Apache 2.0 @@ -301,3 +301,70 @@ sh_test( "REGISTRY_BIN": "$(location @com_github_google_go_containerregistry//cmd/registry)", }, ) + +push_oci( + name = "apply_cloudrun_pushed_image", + image = "//skylib/kustomize/tests:image", + registry = "localhost:1338", + remote_tags = ":apply_tags_file", + repository = "repo/apply_cloudrun_pushed_image", +) + +cloudrun_deploy( + name = "apply_cloudrun_test_target", + project = "ci", + region = "us-central1", + service = "webhook", + gitops = False, + images = { + "//skylib/tests/cloudrun:image": ":apply_cloudrun_pushed_image", + }, + manifests = [ + "//skylib/tests/cloudrun:run.yaml", + ], + visibility = ["//visibility:public"], +) + +sh_test( + name = "cloudrun_apply_test", + size = "small", + srcs = ["cloudrun_apply_test.sh"], + tags = ["exclusive"], + args = [ + "$(location :apply_cloudrun_test_target.apply)", + "$(location :apply_cloudrun_test_target.delete)", + "localhost:1338/repo/apply_cloudrun_pushed_image:testtag", + "localhost:1338/repo/apply_cloudrun_pushed_image", + "webhook", + "ci", + "us-central1", + ], + data = [ + ":apply_cloudrun_test_target.apply", + ":apply_cloudrun_test_target.delete", + "@com_github_google_go_containerregistry//cmd/crane", + "@com_github_google_go_containerregistry//cmd/registry", + ], + env = { + "CRANE_BIN": "$(location @com_github_google_go_containerregistry//cmd/crane)", + "REGISTRY_BIN": "$(location @com_github_google_go_containerregistry//cmd/registry)", + }, +) + +cloudrun_deploy( + name = "prer_cloudrun_target", + project = "ci", + region = "us-central1", + service = "webhook", + deployment_branch = "test1", + gitops = True, + images = { + "//skylib/tests/cloudrun:image": ":apply_cloudrun_pushed_image", + }, + manifests = [ + "//skylib/tests/cloudrun:run.yaml", + ], + release_branch_prefix = "gitops_test_release_branch", + visibility = ["//visibility:public"], +) + diff --git a/gitops/testing/cloudrun_apply_test.sh b/gitops/testing/cloudrun_apply_test.sh new file mode 100755 index 00000000..aec072dc --- /dev/null +++ b/gitops/testing/cloudrun_apply_test.sh @@ -0,0 +1,95 @@ +#!/bin/bash -x +# +# Integration test for cloudrun_deploy .apply and .delete targets using mock gcloud. +# + +apply_bin=$1 +delete_bin=$2 +expected_image_ref=$3 +expected_manifest_sub=$4 +expected_service=$5 +expected_project=$6 +expected_region=$7 + +# Start the local in-memory registry on the default port 1338 +${REGISTRY_BIN} & +registry_pid=$! +trap "kill -9 $registry_pid" EXIT + +# Wait for registry to start up +for i in {1..50}; do + if curl -s -f http://localhost:1338/v2/ >/dev/null; then + break + fi + sleep 0.05 +done + +# Create a temporary directory for the mock gcloud and manifest dump +tmp_dir=$(mktemp -d) +trap "rm -rf $tmp_dir; kill -9 $registry_pid" EXIT + +manifest_out="${tmp_dir}/applied_manifest.yaml" +gcloud_log="${tmp_dir}/gcloud.log" +mock_gcloud="${tmp_dir}/gcloud" + +cat < "${mock_gcloud}" +#!/bin/bash +echo "gcloud \$@" >> "${gcloud_log}" +has_replace=false +for arg in "\$@"; do + if [ "\$arg" = "replace" ]; then + has_replace=true + fi +done + +if [ "\$has_replace" = true ]; then + cat > "${manifest_out}" +fi +EOF + +chmod +x "${mock_gcloud}" + +# Prepend mock gcloud to PATH +export PATH="${tmp_dir}:${PATH}" + +# 1. Execute the apply target executable +${apply_bin} + +# Verify that the image is successfully pushed using crane +${CRANE_BIN} validate -v --fast --remote ${expected_image_ref} + +# Verify that the manifest was applied and has the expected image reference +if [ ! -f "${manifest_out}" ]; then + echo "Error: Applied manifest file was not created by mock gcloud" + exit 1 +fi + +cat "${manifest_out}" + +if ! grep -q "${expected_manifest_sub}" "${manifest_out}"; then + echo "Error: Applied manifest does not contain expected substring '${expected_manifest_sub}'" + exit 1 +fi + +# Verify gcloud apply arguments +expected_apply_args="gcloud run services replace - --project=${expected_project} --region=${expected_region}" +if ! grep -Fq "${expected_apply_args}" "${gcloud_log}"; then + echo "Error: gcloud was not called with the expected replace arguments: ${expected_apply_args}" + echo "gcloud log contents:" + cat "${gcloud_log}" + exit 1 +fi + +# 2. Execute the delete target executable +${delete_bin} + +# Verify gcloud delete arguments +expected_delete_args="gcloud run services delete ${expected_service} --project=${expected_project} --region=${expected_region} --quiet" +if ! grep -Fq "${expected_delete_args}" "${gcloud_log}"; then + echo "Error: gcloud was not called with the expected delete arguments: ${expected_delete_args}" + echo "gcloud log contents:" + cat "${gcloud_log}" + exit 1 +fi + +echo "Success!" diff --git a/skylib/tests/cloudrun/BUILD.bazel b/skylib/tests/cloudrun/BUILD.bazel index 390a2cf0..921f6002 100644 --- a/skylib/tests/cloudrun/BUILD.bazel +++ b/skylib/tests/cloudrun/BUILD.bazel @@ -1,5 +1,7 @@ load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files") -load("@rules_gitops//gitops:defs.bzl", "external_image", "k8s_deploy") +load("@rules_gitops//gitops:defs.bzl", "cloudrun_deploy", "external_image") + +exports_files(["run.yaml"]) external_image( name = "image", @@ -7,13 +9,14 @@ external_image( image = "gcr.io/repo/someimage:thetag", ) -k8s_deploy( +cloudrun_deploy( name = "prod", gitops = False, images = [":image"], manifests = ["run.yaml"], - namespace = "cloudrun_project", - openapi_path = "//skylib:run_schema.json", + project = "cloudrun_project", + region = "us-central1", + service = "webhook", patches = ["run_patch.yaml"], ) From 44e912ba6febb12d35cef4187c69b92e891dfa84 Mon Sep 17 00:00:00 2001 From: Aleksey Pesternikov Date: Thu, 2 Jul 2026 10:06:01 -0700 Subject: [PATCH 2/4] wip --- MODULE.bazel | 1 + MODULE.bazel.lock | 88 +++++++++++++++++++++++++++ README.md | 3 +- gitops/cloudrun.bzl | 51 ++++++---------- gitops/cloudrun.sh.tpl | 1 - gitops/testing/BUILD.bazel | 2 - gitops/testing/cloudrun_apply_test.sh | 2 +- skylib/tests/cloudrun/BUILD.bazel | 1 - 8 files changed, 110 insertions(+), 39 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index a4d344b7..95019230 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -12,6 +12,7 @@ bazel_dep(name = "rules_img", version = "0.3.13") bazel_dep(name = "rules_pkg", version = "1.0.1") bazel_dep(name = "rules_go", version = "0.54.0") bazel_dep(name = "rules_shell", version = "0.8.0") +bazel_dep(name = "yq.bzl", version = "0.3.6") go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") go_sdk.download(version = "1.23.9") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7f0b0e4e..ae2814fc 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -28,6 +28,8 @@ "https://bcr.bazel.build/modules/bazel_features/1.47.0/source.json": "4ba0b5138327f2d73352a51547a4e49a0a828ef400e046b15334d8905bf6b7ff", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -39,6 +41,7 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.0.3/MODULE.bazel": "4791b031727d1183c183a0b8fd5fc0fe8d8834c45f9a22efd26be6c962b9cfee", @@ -63,6 +66,7 @@ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", "https://bcr.bazel.build/modules/package_metadata/0.0.10/MODULE.bazel": "cb66e0ce830e01bc00cd9edc984963704f2759ca893a649996e2b1c5c1ea7271", "https://bcr.bazel.build/modules/package_metadata/0.0.10/source.json": "8fc6bece244828a69b3cc608f381118d67fa3cc2aa1a2851dfe8d99a8076d7d0", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", @@ -162,6 +166,7 @@ "https://bcr.bazel.build/modules/rules_runfiles_group/0.0.1-rc.5/source.json": "32efc01a4b29cbb5b2b97fd1f7d6025e68eb20eea498f1127b068082e315d6cf", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", "https://bcr.bazel.build/modules/rules_shell/0.8.0/MODULE.bazel": "f6a89f1d6a669a26f28fe814503857055d76306b79cfc11d12399af08d0b80ae", "https://bcr.bazel.build/modules/rules_shell/0.8.0/source.json": "eb53cc815bc503c6683c5fe12d943f98883f81fc22f51403ec8a95610cba4195", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", @@ -173,6 +178,8 @@ "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", "https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/yq.bzl/0.3.6/MODULE.bazel": "985c2a0cb4ad9994bb0e33cc7fae931c91105eeefe3faa355b8f4c258d0607c0", + "https://bcr.bazel.build/modules/yq.bzl/0.3.6/source.json": "678aaf6e291164f3cd761bb3e872e8a151248f413dbb63c5524a50b82a5bc890", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", @@ -3223,6 +3230,87 @@ ] ] } + }, + "@@yq.bzl+//yq:extensions.bzl%yq": { + "general": { + "bzlTransitiveDigest": "UfFMy8CWK4/dVo/tfaSAIYUiDGNAPes5eRllx9O9Q9Q=", + "usagesDigest": "HIivRx64D04dq6NJIBX8x2pgz4Az6/Qw39gO1ihBaBU=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "yq_darwin_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "darwin_amd64", + "version": "4.45.2" + } + }, + "yq_darwin_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "darwin_arm64", + "version": "4.45.2" + } + }, + "yq_linux_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_amd64", + "version": "4.45.2" + } + }, + "yq_linux_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_arm64", + "version": "4.45.2" + } + }, + "yq_linux_s390x": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_s390x", + "version": "4.45.2" + } + }, + "yq_linux_riscv64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_riscv64", + "version": "4.45.2" + } + }, + "yq_linux_ppc64le": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_ppc64le", + "version": "4.45.2" + } + }, + "yq_windows_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "windows_amd64", + "version": "4.45.2" + } + }, + "yq_windows_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "windows_arm64", + "version": "4.45.2" + } + }, + "yq_toolchains": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:toolchain.bzl%yq_toolchains_repo", + "attributes": { + "user_repository_name": "yq" + } + } + }, + "recordedRepoMappingEntries": [] + } } }, "facts": {} diff --git a/README.md b/README.md index f9f29dfe..3392ebd6 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ When you run a `.gitops` target, it copies the rendered manifest to the location It accepts similar parameters to `k8s_deploy`, with the following differences: - ***project*** (instead of `namespace`): The GCP project ID. This is also automatically set as the `namespace` field in the Knative Service YAML. - ***region*** (instead of `cluster`): The GCP region (e.g. `us-central1`). -- ***service***: The name of the Cloud Run service. + +Note: There is no `service` parameter. The service name is dynamically resolved at runtime from `metadata.name` inside the rendered manifest. diff --git a/gitops/cloudrun.bzl b/gitops/cloudrun.bzl index cc278297..e44dffe7 100644 --- a/gitops/cloudrun.bzl +++ b/gitops/cloudrun.bzl @@ -68,6 +68,8 @@ def _image_pushes(name_suffix, images, image_registry, image_repository, image_d def _gcloud_run_impl(ctx): files = [] + ctx.files.srcs + yq_bin = ctx.toolchains["@yq.bzl//yq/toolchain:type"].yqinfo.bin + files.append(yq_bin) statements = "" transitive = None transitive_runfiles = [] @@ -80,10 +82,6 @@ def _gcloud_run_impl(ctx): if "{" in ctx.attr.region: region = stamp(ctx, region, files, ctx.label.name + ".region-name", True) - service = ctx.attr.service - if "{" in ctx.attr.service: - service = stamp(ctx, service, files, ctx.label.name + ".service-name", True) - command = ctx.attr.command push = ctx.attr.push @@ -109,13 +107,21 @@ def _gcloud_run_impl(ctx): for inattr in ctx.attr.srcs: for infile in inattr.files.to_list(): if command == "replace": - statements += "{template_engine} --template={infile} --variable=PROJECT=\"$PROJECT\" --variable=REGION=\"$REGION\" --variable=SERVICE=\"$SERVICE\" --stamp_info_file={info_file} | gcloud run services replace - --project=\"$PROJECT\" --region=\"$REGION\"\n".format( + statements += "{template_engine} --template={infile} --variable=PROJECT=\"$PROJECT\" --variable=REGION=\"$REGION\" --stamp_info_file={info_file} | gcloud run services replace - --project=\"$PROJECT\" --region=\"$REGION\"\n".format( infile = get_runfile_path(ctx, infile), template_engine = get_runfile_path(ctx, ctx.executable._template_engine), info_file = get_runfile_path(ctx, ctx.file._info_file), ) elif command == "delete": - statements += "gcloud run services delete \"$SERVICE\" --project=\"$PROJECT\" --region=\"$REGION\" --quiet\n" + statements += ("SERVICE=$({yq_bin_path} '.metadata.name' {infile})\n" + + "if [ -z \"$SERVICE\" ] || [ \"$SERVICE\" = \"null\" ]; then\n" + + " echo \"Error: Failed to extract service name from manifest {infile}\" >&2\n" + + " exit 1\n" + + "fi\n" + + "gcloud run services delete \"$SERVICE\" --project=\"$PROJECT\" --region=\"$REGION\"\n").format( + yq_bin_path = get_runfile_path(ctx, yq_bin), + infile = get_runfile_path(ctx, infile), + ) else: fail("Unsupported command: %s" % command) @@ -124,7 +130,6 @@ def _gcloud_run_impl(ctx): substitutions = { "%{project}": project, "%{region}": region, - "%{service}": service, "%{statements}": statements, }, output = ctx.outputs.executable, @@ -142,7 +147,6 @@ gcloud_run = rule( "srcs": attr.label_list(providers = (GitopsArtifactsInfo,)), "project": attr.string(mandatory = True), "region": attr.string(mandatory = True), - "service": attr.string(mandatory = True), "command": attr.string(default = "replace"), "push": attr.bool(default = True), "_build_user_value": attr.label( @@ -171,6 +175,7 @@ gcloud_run = rule( }, executable = True, implementation = _gcloud_run_impl, + toolchains = ["@yq.bzl//yq/toolchain:type"], ) def _remove_prefix(s, prefix): @@ -183,7 +188,6 @@ def _remove_prefixes(s, prefixes): def _cloudrun_gitops_impl(ctx): region = ctx.attr.region - service = ctx.attr.service project = ctx.attr.project strip_prefixes = ctx.attr.strip_prefixes files = [] @@ -194,8 +198,6 @@ def _cloudrun_gitops_impl(ctx): fi """.format(push_statements) - if "{" in service: - fail("unable to gitops service with placeholders %s" % service) if "{" in region: fail("unable to gitops region with placeholders %s" % region) if "{" in project: @@ -206,14 +208,13 @@ fi statements += ("echo $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n" + "mkdir -p $TARGET_DIR/{gitops_path}/{project}/{region}\n" + "echo '# GENERATED BY {rulename} -> {gitopsrulename}' > $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n" + - "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --variable=SERVICE={service} --stamp_info_file={info_file} >> $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n").format( + "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --stamp_info_file={info_file} >> $TARGET_DIR/{gitops_path}/{project}/{region}/{file}\n").format( infile = get_runfile_path(ctx, infile), rulename = inattr.label, gitopsrulename = ctx.label, project = project, gitops_path = ctx.attr.gitops_path, region = region, - service = service, file = _remove_prefixes(infile.path.split("/")[-1], strip_prefixes), template_engine = get_runfile_path(ctx, ctx.executable._template_engine), info_file = get_runfile_path(ctx, ctx.file._info_file), @@ -246,7 +247,6 @@ cloudrun_gitops = rule( "srcs": attr.label_list(providers = (GitopsArtifactsInfo,)), "project": attr.string(mandatory = True), "region": attr.string(mandatory = True), - "service": attr.string(mandatory = True), "deployment_branch": attr.string(), "gitops_path": attr.string(), "release_branch_prefix": attr.string(), @@ -288,14 +288,13 @@ RUNFILES=${RUNFILES:-$(guess_runfiles)} """ outputs = [] - script_template = "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --variable=SERVICE={service} --stamp_info_file={info_file}\n" + script_template = "{template_engine} --template={infile} --variable=PROJECT={project} --variable=REGION={region} --stamp_info_file={info_file}\n" for dep in ctx.attr.src.files.to_list(): outputs.append(script_template.format( infile = get_runfile_path(ctx, dep), template_engine = get_runfile_path(ctx, ctx.executable._template_engine), project = ctx.attr.project, region = ctx.attr.region, - service = ctx.attr.service, info_file = get_runfile_path(ctx, ctx.file._info_file), )) @@ -319,9 +318,6 @@ cloudrun_show = rule( "region": attr.string( mandatory = True, ), - "service": attr.string( - mandatory = True, - ), "_info_file": attr.label( default = Label("//skylib:more_stable_status.txt"), allow_single_file = True, @@ -339,7 +335,6 @@ def cloudrun_deploy( name, project = None, region = None, - service = None, configmaps_srcs = None, secrets_srcs = None, configmaps_renaming = None, @@ -358,7 +353,7 @@ def cloudrun_deploy( deps_aliases = {}, images = [], image_digest_tag = False, - image_registry = "docker.io", + image_registry = "gcr.io", image_repository = None, gitops = True, gitops_path = "cloud", @@ -371,13 +366,12 @@ def cloudrun_deploy( if not manifests: manifests = native.glob(["*.yaml", "*.yaml.tpl"]) - for reservedname in ["PROJECT", "REGION", "SERVICE"]: + for reservedname in ["PROJECT", "REGION"]: if substitutions.get(reservedname): fail("do not put %s in substitutions parameter of cloudrun_deploy. It will be added autimatically" % reservedname) substitutions = dict(substitutions) substitutions["PROJECT"] = project substitutions["REGION"] = region - substitutions["SERVICE"] = service if not gitops: image_pushes = _image_pushes( @@ -418,7 +412,6 @@ def cloudrun_deploy( srcs = [name], project = project, region = region, - service = service, command = "replace", tags = tags, visibility = visibility, @@ -429,7 +422,6 @@ def cloudrun_deploy( command = "delete", project = project, region = region, - service = service, push = False, tags = tags, visibility = visibility, @@ -439,13 +431,10 @@ def cloudrun_deploy( src = name, project = project, region = region, - service = service, tags = tags, visibility = visibility, ) else: - if not service: - fail("service must be defined for gitops cloudrun_deploy") if not region: fail("region must be defined for gitops cloudrun_deploy") if not project: @@ -489,7 +478,6 @@ def cloudrun_deploy( srcs = [name], project = project, region = region, - service = service, command = "replace", tags = tags, visibility = visibility, @@ -500,7 +488,6 @@ def cloudrun_deploy( command = "delete", project = project, region = region, - service = service, push = False, tags = tags, visibility = visibility, @@ -510,10 +497,9 @@ def cloudrun_deploy( srcs = [name], project = project, region = region, - service = service, gitops_path = gitops_path, strip_prefixes = [ - service + "-", + project + "-", region + "-", ], deployment_branch = deployment_branch, @@ -526,7 +512,6 @@ def cloudrun_deploy( src = name, project = project, region = region, - service = service, tags = tags, visibility = visibility, ) diff --git a/gitops/cloudrun.sh.tpl b/gitops/cloudrun.sh.tpl index bd34346a..ec5a80fa 100644 --- a/gitops/cloudrun.sh.tpl +++ b/gitops/cloudrun.sh.tpl @@ -33,6 +33,5 @@ function waitpids() { PROJECT=%{project} REGION=%{region} -SERVICE=%{service} %{statements} diff --git a/gitops/testing/BUILD.bazel b/gitops/testing/BUILD.bazel index 8b9b79d4..2e54eeb1 100644 --- a/gitops/testing/BUILD.bazel +++ b/gitops/testing/BUILD.bazel @@ -314,7 +314,6 @@ cloudrun_deploy( name = "apply_cloudrun_test_target", project = "ci", region = "us-central1", - service = "webhook", gitops = False, images = { "//skylib/tests/cloudrun:image": ":apply_cloudrun_pushed_image", @@ -355,7 +354,6 @@ cloudrun_deploy( name = "prer_cloudrun_target", project = "ci", region = "us-central1", - service = "webhook", deployment_branch = "test1", gitops = True, images = { diff --git a/gitops/testing/cloudrun_apply_test.sh b/gitops/testing/cloudrun_apply_test.sh index aec072dc..d0660a01 100755 --- a/gitops/testing/cloudrun_apply_test.sh +++ b/gitops/testing/cloudrun_apply_test.sh @@ -84,7 +84,7 @@ fi ${delete_bin} # Verify gcloud delete arguments -expected_delete_args="gcloud run services delete ${expected_service} --project=${expected_project} --region=${expected_region} --quiet" +expected_delete_args="gcloud run services delete ${expected_service} --project=${expected_project} --region=${expected_region}" if ! grep -Fq "${expected_delete_args}" "${gcloud_log}"; then echo "Error: gcloud was not called with the expected delete arguments: ${expected_delete_args}" echo "gcloud log contents:" diff --git a/skylib/tests/cloudrun/BUILD.bazel b/skylib/tests/cloudrun/BUILD.bazel index 921f6002..c96e6ecd 100644 --- a/skylib/tests/cloudrun/BUILD.bazel +++ b/skylib/tests/cloudrun/BUILD.bazel @@ -16,7 +16,6 @@ cloudrun_deploy( manifests = ["run.yaml"], project = "cloudrun_project", region = "us-central1", - service = "webhook", patches = ["run_patch.yaml"], ) From af15db428bc4545ab17f9595d3ce81f18d4591c5 Mon Sep 17 00:00:00 2001 From: Aleksey Pesternikov Date: Thu, 2 Jul 2026 11:28:03 -0700 Subject: [PATCH 3/4] refactor: remove configmap and secret parameters from cloudrun rule --- gitops/cloudrun.bzl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/gitops/cloudrun.bzl b/gitops/cloudrun.bzl index e44dffe7..f26627b4 100644 --- a/gitops/cloudrun.bzl +++ b/gitops/cloudrun.bzl @@ -335,9 +335,6 @@ def cloudrun_deploy( name, project = None, region = None, - configmaps_srcs = None, - secrets_srcs = None, - configmaps_renaming = None, manifests = None, name_prefix = None, name_suffix = None, @@ -385,9 +382,6 @@ def cloudrun_deploy( kustomize( name = name, namespace = project, - configmaps_srcs = configmaps_srcs, - secrets_srcs = secrets_srcs, - disable_name_suffix_hash = (configmaps_renaming != "hash"), images = image_pushes, manifests = manifests, substitutions = substitutions, @@ -451,9 +445,6 @@ def cloudrun_deploy( kustomize( name = name, namespace = project, - configmaps_srcs = configmaps_srcs, - secrets_srcs = secrets_srcs, - disable_name_suffix_hash = (configmaps_renaming != "hash"), images = image_pushes, manifests = manifests, visibility = visibility, From 2bdd82d93e2ccdcc403c738d470951cfe3c42e17 Mon Sep 17 00:00:00 2001 From: Aleksey Pesternikov Date: Thu, 2 Jul 2026 15:00:54 -0700 Subject: [PATCH 4/4] multiple objects support in .delete --- gitops/cloudrun.bzl | 16 ++++++++------- gitops/testing/BUILD.bazel | 4 +++- gitops/testing/cloudrun_apply_test.sh | 29 ++++++++++++++++++--------- gitops/testing/run_second.yaml | 14 +++++++++++++ 4 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 gitops/testing/run_second.yaml diff --git a/gitops/cloudrun.bzl b/gitops/cloudrun.bzl index f26627b4..c0320ddc 100644 --- a/gitops/cloudrun.bzl +++ b/gitops/cloudrun.bzl @@ -113,15 +113,17 @@ def _gcloud_run_impl(ctx): info_file = get_runfile_path(ctx, ctx.file._info_file), ) elif command == "delete": - statements += ("SERVICE=$({yq_bin_path} '.metadata.name' {infile})\n" + - "if [ -z \"$SERVICE\" ] || [ \"$SERVICE\" = \"null\" ]; then\n" + - " echo \"Error: Failed to extract service name from manifest {infile}\" >&2\n" + + statements += ("SERVICES=$({yq_bin_path} 'select(.kind == \"Service\") | .metadata.name' {infile})\n" + + "if [ -z \"$SERVICES\" ] || [ \"$SERVICES\" = \"null\" ]; then\n" + + " echo \"Error: Failed to extract any service names from manifest {infile}\" >&2\n" + " exit 1\n" + "fi\n" + - "gcloud run services delete \"$SERVICE\" --project=\"$PROJECT\" --region=\"$REGION\"\n").format( - yq_bin_path = get_runfile_path(ctx, yq_bin), - infile = get_runfile_path(ctx, infile), - ) + "for SERVICE in $SERVICES; do\n" + + " gcloud run services delete \"$SERVICE\" --project=\"$PROJECT\" --region=\"$REGION\"\n" + + "done\n").format( + yq_bin_path = get_runfile_path(ctx, yq_bin), + infile = get_runfile_path(ctx, infile), + ) else: fail("Unsupported command: %s" % command) diff --git a/gitops/testing/BUILD.bazel b/gitops/testing/BUILD.bazel index 2e54eeb1..570b49b1 100644 --- a/gitops/testing/BUILD.bazel +++ b/gitops/testing/BUILD.bazel @@ -320,6 +320,7 @@ cloudrun_deploy( }, manifests = [ "//skylib/tests/cloudrun:run.yaml", + ":run_second.yaml", ], visibility = ["//visibility:public"], ) @@ -334,9 +335,10 @@ sh_test( "$(location :apply_cloudrun_test_target.delete)", "localhost:1338/repo/apply_cloudrun_pushed_image:testtag", "localhost:1338/repo/apply_cloudrun_pushed_image", - "webhook", "ci", "us-central1", + "webhook", + "webhook-secondary", ], data = [ ":apply_cloudrun_test_target.apply", diff --git a/gitops/testing/cloudrun_apply_test.sh b/gitops/testing/cloudrun_apply_test.sh index d0660a01..b6077b88 100755 --- a/gitops/testing/cloudrun_apply_test.sh +++ b/gitops/testing/cloudrun_apply_test.sh @@ -7,9 +7,9 @@ apply_bin=$1 delete_bin=$2 expected_image_ref=$3 expected_manifest_sub=$4 -expected_service=$5 -expected_project=$6 -expected_region=$7 +expected_project=$5 +expected_region=$6 +expected_services="${@:7}" # Start the local in-memory registry on the default port 1338 ${REGISTRY_BIN} & @@ -80,16 +80,25 @@ if ! grep -Fq "${expected_apply_args}" "${gcloud_log}"; then exit 1 fi +num_replaces=$(grep -c "gcloud run services replace" "${gcloud_log}") +if [ "${num_replaces}" -ne 1 ]; then + echo "Error: expected gcloud run services replace to be called exactly once, but it was called ${num_replaces} times" + cat "${gcloud_log}" + exit 1 +fi + # 2. Execute the delete target executable ${delete_bin} # Verify gcloud delete arguments -expected_delete_args="gcloud run services delete ${expected_service} --project=${expected_project} --region=${expected_region}" -if ! grep -Fq "${expected_delete_args}" "${gcloud_log}"; then - echo "Error: gcloud was not called with the expected delete arguments: ${expected_delete_args}" - echo "gcloud log contents:" - cat "${gcloud_log}" - exit 1 -fi +for service in ${expected_services}; do + expected_delete_args="gcloud run services delete ${service} --project=${expected_project} --region=${expected_region}" + if ! grep -Fq "${expected_delete_args}" "${gcloud_log}"; then + echo "Error: gcloud was not called with the expected delete arguments: ${expected_delete_args}" + echo "gcloud log contents:" + cat "${gcloud_log}" + exit 1 + fi +done echo "Success!" diff --git a/gitops/testing/run_second.yaml b/gitops/testing/run_second.yaml new file mode 100644 index 00000000..c2f9a91d --- /dev/null +++ b/gitops/testing/run_second.yaml @@ -0,0 +1,14 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: webhook-secondary + labels: + cloud.googleapis.com/location: us-central1 +spec: + template: + spec: + containers: + - name: main + image: //skylib/tests/cloudrun:image + ports: + - containerPort: 8080