tools/build-image.py (186 lines of code) (raw):

#!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # 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 CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import getopt import getpass import json import os import shutil import subprocess import sys # Supported host architectures for executables and docker images # Mapped to the correct settings in the Makefile architecture = {"x86_64": "amd64", "aarch64": "arm64"} # Make targets for the shim repo to generate the images targets = {"adm_image": "admission", "sched_image": "scheduler", "plugin_image": "scheduler-plugin"} # registry setting passed to Makefile to allow testing of the script repository = "apache" # authentication info for docker hub docker_user = "" docker_pass = "" docker_token = "" # fail the execution def fail(message): print(message) sys.exit(1) # get the command from the path def get_cmd(name): cmd = shutil.which(name) if not cmd: fail("command not found on the path: '%s'" % name) return cmd # load the config, based on the build-release.py code. def load_config(): tools_dir = os.path.dirname(os.path.realpath(__file__)) # load configs config_file = os.path.join(tools_dir, "release-configs.json") with open(config_file) as configs: try: data = json.load(configs) except json.JSONDecodeError: fail("load config: unexpected json decode failure") if "release" not in data: fail("load config: release data not found") release_meta = data["release"] if "version" not in release_meta: fail("load config: version data not found in release") version = release_meta["version"] release_package_name = "apache-yunikorn-{0}-src".format(version) if "repositories" not in data: fail("load config: repository list not found") repo_list = data["repositories"] staging_dir = os.path.join(os.path.dirname(tools_dir), "staging") release_base = os.path.join(staging_dir, release_package_name) print("release meta info:") print(" - version: %s" % version) print(" - base directory: %s" % release_base) print(" - package name: %s" % release_package_name) if not os.path.exists(release_base): fail("Staged release dir does not exist:\n\t%s" % release_base) return version, repo_list, release_base # Cleanup image tag def remove_tag(image_name): splits = image_name.split(":") if len(splits) != 2: fail("Image name is not in the required format") cmd = get_cmd("curl") curl = [cmd, "-X", "DELETE", "-H", "Authorization: JWT " + docker_token] curl.extend(["https://hub.docker.com/v2/repositories/" + splits[0] + "/tags/" + splits[1] + "/"]) retcode = subprocess.call(curl) if retcode: fail("docker tag cleanup failed") # Push an image or manifest def push_image(cmd, image_name): push = [cmd, "push", image_name] retcode = subprocess.call(push, stdout=subprocess.DEVNULL) if retcode: fail("docker push failed") # get token for rest # 2FA is not supported by this code (yet) but can be added def get_token(): cmd = get_cmd("curl") curl = [cmd, "-X", "POST", "-H", "Content-Type: application/json"] curl.extend(["-d", '{"username": "' + docker_user + '", "password": "' + docker_pass + '"}']) curl.extend(["https://hub.docker.com/v2/users/login/"]) p = subprocess.run(curl, capture_output=True) try: data = json.loads(p.stdout) except json.JSONDecodeError: fail("login failed: unexpected json decode failure: %s" %p.stdout) if "detail" in data: fail("authentication failed: %s" % data["detail"]) if "token" not in data: fail("login failed: unexpected json content: %s" % data) global docker_token docker_token = data["token"] # get user and password on startup def get_auth(): global docker_user docker_user = input("Enter docker hub username: ") global docker_pass docker_pass = getpass.getpass(prompt="Docker hub password: ", stream=None) if docker_pass == "" or docker_user == "": fail("username and password required") # Login to docker def login(): cmd = get_cmd("docker") # login to docker print("Login to docker hub") log_in = [cmd, "login", "--username", docker_user, "--password", docker_pass] retcode = subprocess.call(log_in, stdout=subprocess.DEVNULL) if retcode: fail("docker login failed") get_token() # Create an image name based on passed in details def create_image_name(image, version, arch): image_name = repository + "/yunikorn:" + image if arch != "": image_name += "-" + arch image_name += "-" + version return image_name # Create the manifest def build_manifest(manifest, version): print("Building manifest") print(" - manifest: %s" % manifest) print(" - version: %s" % version) multi_image = create_image_name(manifest, version, "") cmd = get_cmd("docker") command = [cmd, "manifest", "create", multi_image] for arch in architecture: image_name = create_image_name(manifest, version, architecture[arch]) print(" - image: %s" % image_name) # image_manifest = create_image_name(manifest, version, manifestmap[arch]) # print(" - image manifest: %s" % image_manifest) # temporary push to create tag to allow manifest build # https://github.com/docker/cli/issues/3350 push_image(cmd, image_name) command.extend(["--amend", image_name]) retcode = subprocess.call(command, stdout=subprocess.DEVNULL) if retcode: fail("docker manifest creation failed") # push the manifest # purge option is needed: https://github.com/docker/cli/issues/954 command = [cmd, "manifest", "push", "--purge", multi_image] retcode = subprocess.call(command, stdout=subprocess.DEVNULL) if retcode: fail("docker manifest push failed") # remove temporary tags that allowed manifest build for arch in architecture: image_name = create_image_name(manifest, version, architecture[arch]) remove_tag(image_name) # Build a scheduler image def build_image(base_dir, image, arch, version): cmd = get_cmd("make") my_env = os.environ.copy() my_env["QUIET"] = "--quiet" # stop image build from being chatty my_env["VERSION"] = version # force version, just be safe my_env["HOST_ARCH"] = arch # the architecture override my_env["REGISTRY"] = repository # repository override (test only) command = [cmd, "clean", image] # build the image using make retcode = subprocess.call(command, cwd=base_dir, env=my_env, stdout=subprocess.DEVNULL) if retcode: fail("make image failed") # Build the web image def web_image(base_dir, version): # build the images for arch in architecture: print("Building image for 'web', using 'image', architecture: '%s'" % arch) build_image(base_dir, "image", arch, version) # build the manifest build_manifest("web", version) # Build the scheduler images def scheduler_images(base_dir, version): # build the images for each target for target in targets: image = targets[target] # build all architectures for arch in architecture: print("Building image '%s' using: '%s', architecture: '%s'" % (image, target, arch)) build_image(base_dir, target, arch, version) # build the manifest build_manifest(image, version) # Build the combined architecture images def build_images(): get_auth() login() version, repo_list, release_base = load_config() for repo_meta in repo_list: if "name" not in repo_meta: fail("repository name missing in repo list") repo_name = repo_meta["name"] switcher = { "yunikorn-k8shim": scheduler_images, "yunikorn-web": web_image, } if switcher.get(repo_name) is not None: if "alias" not in repo_meta: fail("repository alias missing in repo list") alias = repo_meta["alias"] switcher.get(repo_name)(os.path.join(release_base, alias), version) # Print the usage info def usage(script): print("%s [--repository <name>]" % script) print("repository override should only be used for testing") sys.exit(2) def main(argv): script = argv[0] try: opts, args = getopt.getopt(argv[1:], "", ["repository="]) except getopt.GetoptError: usage(script) if args: usage(script) global repository for opt, arg in opts: if opt == "--repository": if not arg: usage(script) repository = arg build_images() if __name__ == "__main__": main(sys.argv)