diff --git a/.github/workflows/release-mongodb.yml b/.github/workflows/release-mongodb.yml new file mode 100644 index 000000000..0d423fbf8 --- /dev/null +++ b/.github/workflows/release-mongodb.yml @@ -0,0 +1,70 @@ +name: Release Single Mongodb Image +on: + workflow_dispatch: + inputs: + pipeline-argument: + description: 'Images(agent,readiness-probe,version-upgrade-hook,operator)' + required: true + image-tag: + description: 'Image tag to build' + required: true +jobs: + release-single-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + #/usr/bin/git config --global --add safe.directory /home/runner/work/ksmartdata/ksmartdata/ksmartdata + path: ksmartdata + + - name: Checkout mongodb operator + uses: actions/checkout@v4 + with: + repository: "mongodb/mongodb-kubernetes-operator" + # 使用指定的tag + ref: 'v0.10.0' + #/usr/bin/git config --global --add safe.directory /home/runner/work/ksmartdata/ksmartdata/mongodb-kubernetes-operator + path: mongodb-kubernetes-operator + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10.4' + architecture: 'x64' + + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ hashFiles('./mongodb-kubernetes-operator/requirements.txt') }} + + - name: Install Python Dependencies + run: pip install -r ./mongodb-kubernetes-operator/requirements.txt + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # template: .action_templates/steps/set-up-qemu.yaml + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Override some files of mongodb operator + run: | + cp ./ksmartdata/images/mongodb/config.json ./mongodb-kubernetes-operator/scripts/ci/config.json + cp ./ksmartdata/images/mongodb/operator-inventory.yaml ./mongodb-kubernetes-operator/inventories/operator-inventory.yaml + cp ./ksmartdata/images/mongodb/pipeline.py ./mongodb-kubernetes-operator/pipeline.py + + - name: Build and Push Image To ghcr.io + working-directory: ./mongodb-kubernetes-operator + run: python pipeline.py --image-name ${{ github.event.inputs.pipeline-argument }} --tag ${{ github.event.inputs.image-tag }} + env: + MONGODB_COMMUNITY_CONFIG: "${{ github.workspace }}/mongodb-kubernetes-operator/scripts/ci/config.json" + version_id: "${{ github.event.inputs.image-tag }}" + diff --git a/images/mongodb/config.json b/images/mongodb/config.json new file mode 100644 index 000000000..184d838f4 --- /dev/null +++ b/images/mongodb/config.json @@ -0,0 +1,9 @@ +{ + "namespace": "default", + "repo_url": "ghcr.io/usernameisnull", + "operator_image": "mongodb-kubernetes-operator", + "version_upgrade_hook_image": "mongodb-kubernetes-operator-version-upgrade-post-start-hook", + "agent_image": "mongodb-agent-ubi", + "readiness_probe_image": "mongodb-kubernetes-readinessprobe", + "s3_bucket": "" +} diff --git a/images/mongodb/operator-inventory.yaml b/images/mongodb/operator-inventory.yaml new file mode 100644 index 000000000..6eb916819 --- /dev/null +++ b/images/mongodb/operator-inventory.yaml @@ -0,0 +1,117 @@ +vars: + registry: + architecture: amd64 + +images: + - name: operator + vars: + context: . + template_context: scripts/dev/templates/operator + + inputs: + - image + - image_dev + + platform: linux/$(inputs.params.architecture) + + stages: +# +# Dev build stages +# + - name: operator-builder-dev + task_type: docker_build + tags: [ "ubi" ] + dockerfile: scripts/dev/templates/operator/Dockerfile.builder + + buildargs: + builder_image: $(inputs.params.builder_image) + + labels: + quay.expires-after: 48h + + output: + - registry: $(inputs.params.registry)/$(inputs.params.image_dev) + tag: $(inputs.params.version_id)-context-$(inputs.params.architecture) + + - name: operator-template-dev + task_type: dockerfile_template + tags: ["ubi"] + template_file_extension: operator + inputs: + - base_image + + output: + - dockerfile: scripts/dev/templates/operator/Dockerfile.operator-$(inputs.params.version_id) + + - name: operator-build-dev + task_type: docker_build + tags: ["ubi"] + dockerfile: scripts/dev/templates/operator/Dockerfile.operator-$(inputs.params.version_id) + + inputs: + - version_id + + buildargs: + imagebase: $(inputs.params.registry)/$(inputs.params.image_dev):$(inputs.params.version_id)-context-$(inputs.params.architecture) + + labels: + quay.expires-after: 48h + + output: + - registry: $(inputs.params.registry)/$(inputs.params.image_dev) + tag: $(inputs.params.version_id)-$(inputs.params.architecture) + - registry: $(inputs.params.registry)/$(inputs.params.image_dev) + tag: latest-$(inputs.params.architecture) + +# +# Release build stages +# + - name: operator-builder-release + task_type: docker_build + tags: [ "ubi", "release"] + + inputs: + - builder_image + - release_version + + dockerfile: scripts/dev/templates/operator/Dockerfile.builder + + labels: + quay.expires-after: Never + + buildargs: + builder_image: $(inputs.params.builder_image) + + output: + - registry: $(inputs.params.registry)/$(inputs.params.image) + tag: $(inputs.params.release_version)-context-$(inputs.params.architecture) + + - name: operator-template-release + task_type: dockerfile_template + tags: [ "ubi", "release"] + template_file_extension: operator + inputs: + - base_image + - release_version + + output: + - dockerfile: scripts/dev/templates/operator/Dockerfile.operator-$(inputs.params.release_version) + + - name: operator-build-release + task_type: docker_build + tags: [ "ubi", "release"] + + inputs: + - release_version + + dockerfile: scripts/dev/templates/operator/Dockerfile.operator-$(inputs.params.release_version) + + buildargs: + imagebase: $(inputs.params.registry)/$(inputs.params.image):$(inputs.params.release_version)-context-$(inputs.params.architecture) + + labels: + quay.expires-after: Never + + output: + - registry: $(inputs.params.registry)/$(inputs.params.image) + tag: $(inputs.params.release_version)-$(inputs.params.architecture) diff --git a/images/mongodb/pipeline.py b/images/mongodb/pipeline.py new file mode 100644 index 000000000..f1ac289a7 --- /dev/null +++ b/images/mongodb/pipeline.py @@ -0,0 +1,291 @@ +import argparse +import json +import subprocess +import sys +from typing import Dict, List, Set +from scripts.ci.base_logger import logger +from scripts.ci.images_signing import ( + sign_image, + verify_signature, + mongodb_artifactory_login, +) + +from scripts.dev.dev_config import load_config, DevConfig +from sonar.sonar import process_image + +# These image names must correspond to prefixes in release.json, developer configuration and inventories +VALID_IMAGE_NAMES = { + "agent", + "readiness-probe", + "version-upgrade-hook", + "operator", + "e2e", +} + +AGENT_DISTRO_KEY = "agent_distro" +TOOLS_DISTRO_KEY = "tools_distro" + +AGENT_DISTROS_PER_ARCH = { + "amd64": {AGENT_DISTRO_KEY: "rhel7_x86_64", TOOLS_DISTRO_KEY: "rhel70-x86_64"}, + "arm64": {AGENT_DISTRO_KEY: "amzn2_aarch64", TOOLS_DISTRO_KEY: "rhel82-aarch64"}, +} + + +def load_release() -> Dict: + with open("release.json") as f: + return json.load(f) + + +def build_image_args(config: DevConfig, image_name: str) -> Dict[str, str]: + release = load_release() + + # Naming in pipeline : readiness-probe, naming in dev config : readiness_probe_image + image_name_prefix = image_name.replace("-", "_") + + # Default config + arguments = { + "builder": "true", + # Defaults to "" if empty, e2e has no release version + "release_version": release.get(image_name, ""), + "tools_version": "", + "image": getattr(config, f"{image_name_prefix}_image"), + # Defaults to "" if empty, e2e has no dev image + "image_dev": getattr(config, f"{image_name_prefix}_image_dev", ""), + "registry": config.repo_url, + "s3_bucket": config.s3_bucket, + "builder_image": release["golang-builder-image"], + "base_image": "registry.access.redhat.com/ubi8/ubi-minimal:8.6-994", + "inventory": "inventory.yaml", + "skip_tags": config.skip_tags, # Include skip_tags + "include_tags": config.include_tags, # Include include_tags + } + + # Handle special cases + if image_name == "operator": + arguments["inventory"] = "inventories/operator-inventory.yaml" + + if image_name == "e2e": + arguments.pop("builder", None) + arguments["base_image"] = release["golang-builder-image"] + arguments["inventory"] = "inventories/e2e-inventory.yaml" + + if image_name == "agent": + arguments["tools_version"] = release["agent-tools-version"] + + return arguments + + +def sign_and_verify(registry: str, tag: str) -> None: + sign_image(registry, tag) + verify_signature(registry, tag) + + +def build_and_push_image( + image_name: str, + config: DevConfig, + args: Dict[str, str], + architectures: Set[str], + release: bool, + sign: bool, +) -> None: + + logger.info(f"args = {args}, image_name = {image_name}, config = {config}") + + if sign: + mongodb_artifactory_login() + for arch in architectures: + image_tag = f"{image_name}" + args["architecture"] = arch + if image_name == "agent": + args[AGENT_DISTRO_KEY] = AGENT_DISTROS_PER_ARCH[arch][AGENT_DISTRO_KEY] + args[TOOLS_DISTRO_KEY] = AGENT_DISTROS_PER_ARCH[arch][TOOLS_DISTRO_KEY] + process_image( + image_tag, + build_args=args, + inventory=args["inventory"], + skip_tags=args["skip_tags"], + include_tags=args["include_tags"], + ) + if release: + registry = args["registry"] + "/" + args["image"] + context_tag = args["release_version"] + "-context-" + arch + release_tag = args["release_version"] + "-" + arch + if sign: + sign_and_verify(registry, context_tag) + sign_and_verify(registry, release_tag) + + if args["image_dev"]: + image_to_push = args["image_dev"] + elif image_name == "e2e": + # If no image dev (only e2e is concerned) we push the normal image + image_to_push = args["image"] + else: + raise Exception("Dev image must be specified") + + push_manifest(config, architectures, image_to_push) + + if config.gh_run_id: + push_manifest(config, architectures, image_to_push, config.gh_run_id) + + if release: + registry = args["registry"] + "/" + args["image"] + context_tag = args["release_version"] + "-context" + push_manifest(config, architectures, args["image"], args["release_version"]) + push_manifest(config, architectures, args["image"], context_tag) + if sign: + sign_and_verify(registry, args["release_version"]) + sign_and_verify(registry, context_tag) + + +""" +Generates docker manifests by running the following commands: +1. Clear existing manifests +docker manifest rm config.repo_url/image:tag +2. Create the manifest +docker manifest create config.repo_url/image:tag --amend config.repo_url/image:tag-amd64 --amend config.repo_url/image:tag-arm64 +3. Push the manifest +docker manifest push config.repo_url/image:tag +""" + + +def push_manifest( + config: DevConfig, + architectures: Set[str], + image_name: str, + image_tag: str = "latest", +) -> None: + logger.info(f"Pushing manifest for {image_tag}") + final_manifest = "{0}/{1}:{2}".format(config.repo_url, image_name, image_tag) + remove_args = ["docker", "manifest", "rm", final_manifest] + logger.info("Removing existing manifest") + run_cli_command(remove_args, fail_on_error=False) + + create_args = [ + "docker", + "manifest", + "create", + final_manifest, + ] + + for arch in architectures: + create_args.extend(["--amend", final_manifest + "-" + arch]) + + logger.info("Creating new manifest") + run_cli_command(create_args) + + push_args = ["docker", "manifest", "push", final_manifest] + logger.info("Pushing new manifest") + run_cli_command(push_args) + + +# Raises exceptions by default +def run_cli_command(args: List[str], fail_on_error: bool = True) -> None: + command = " ".join(args) + logger.debug(f"Running: {command}") + try: + cp = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + check=False, + ) + except Exception as e: + logger.error(f" Command raised the following exception: {e}") + if fail_on_error: + raise Exception + else: + logger.warning("Continuing...") + return + + if cp.returncode != 0: + error_msg = cp.stderr.decode().strip() + stdout = cp.stdout.decode().strip() + logger.error(f"Error running command") + logger.error(f"stdout:\n{stdout}") + logger.error(f"stderr:\n{error_msg}") + if fail_on_error: + raise Exception + else: + logger.warning("Continuing...") + return + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--image-name", type=str) + parser.add_argument("--release", action="store_true", default=False) + parser.add_argument( + "--arch", + choices=["amd64", "arm64"], + nargs="+", + help="for daily builds only, specify the list of architectures to build for images", + ) + parser.add_argument("--tag", type=str) + parser.add_argument("--sign", action="store_true", default=False) + return parser.parse_args() + + +""" +Takes arguments: +--image-name : The name of the image to build, must be one of VALID_IMAGE_NAMES +--release : We push the image to the registry only if this flag is set +--architecture : List of architectures to build for the image +--sign : Sign images with our private key if sign is set (only for release) + +Run with --help for more information +Example usage : `python pipeline.py --image-name agent --release --sign` + +Builds and push the docker image to the registry +Many parameters are defined in the dev configuration, default path is : ~/.community-operator-dev/config.json +""" + + +def main() -> int: + args = _parse_args() + + image_name = args.image_name + if image_name not in VALID_IMAGE_NAMES: + logger.error( + f"Invalid image name: {image_name}. Valid options are: {VALID_IMAGE_NAMES}" + ) + return 1 + + # Handle dev config + config: DevConfig = load_config() + config.gh_run_id = args.tag + + # Warn user if trying to release E2E tests + if args.release and image_name == "e2e": + logger.warning( + "Warning : releasing E2E test will fail because E2E image has no release version" + ) + + # Skipping release tasks by default + if not args.release: + config.ensure_skip_tag("release") + if args.sign: + logger.warning("--sign flag has no effect without --release") + + if args.arch: + arch_set = set(args.arch) + else: + # Default is multi-arch + arch_set = {"amd64", "arm64"} + logger.info(f"Building for architectures: {','.join(arch_set)}") + + if not args.sign: + logger.warning("--sign flag not provided, images won't be signed") + + image_args = build_image_args(config, image_name) + + logger.info("image_args = {image_args}") + + build_and_push_image( + image_name, config, image_args, arch_set, args.release, args.sign + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())