|
| 1 | +import argparse |
| 2 | +import base64 |
| 3 | +import logging |
| 4 | +import json |
| 5 | +import os |
| 6 | +import re |
| 7 | +import subprocess |
| 8 | +import sys |
| 9 | +import yaml |
| 10 | + |
| 11 | +from datetime import datetime |
| 12 | +from urllib.parse import urljoin |
| 13 | + |
| 14 | +from jinja2 import FileSystemLoader, Environment, StrictUndefined |
| 15 | + |
| 16 | + |
| 17 | +logger = logging.getLogger(__name__) |
| 18 | + |
| 19 | +LOGO_DOWNSTREAM_FILE = "img/downstream_logo.png" |
| 20 | +LOGO_UPSTREAM_FILE = "img/upstream_logo.png" |
| 21 | + |
| 22 | +PACKAGE_NAME = "container-security-operator" |
| 23 | + |
| 24 | +# Default location for the image |
| 25 | +REGISTRY_HOST = "quay.io" |
| 26 | +REGISTRY_API_BASE = REGISTRY_HOST + "/api/v1/" |
| 27 | + |
| 28 | +CSO_REPO = "projectquay/" + PACKAGE_NAME |
| 29 | +CSO_IMAGE = REGISTRY_HOST + "/" + CSO_REPO |
| 30 | +CSO_IMAGE_TAG = "master" |
| 31 | + |
| 32 | +CSO_CATALOG_REPO = "projectquay/cso-catalog" |
| 33 | +CSO_CATALOG_IMAGE = REGISTRY_HOST + "/" + CSO_CATALOG_REPO |
| 34 | +CSO_CATALOG_IMAGE_TAG = "master" |
| 35 | + |
| 36 | +# Default template values |
| 37 | +K8S_API_VERSION = "v1alpha1" |
| 38 | + |
| 39 | +# Jinja templates |
| 40 | +TEMPLATE_DIR = "templates" |
| 41 | +CSV_TEMPLATE_FILE = PACKAGE_NAME + ".clusterserviceversion.yaml.jnj" |
| 42 | +CRD_TEMPLATE_FILES = [ |
| 43 | + "imagemanifestvulns.secscan.quay.redhat.com.crd.yaml.jnj" |
| 44 | +] |
| 45 | + |
| 46 | +# Output |
| 47 | +OUTPUT_MANIFEST_DIR = os.path.join("manifests", PACKAGE_NAME) |
| 48 | +OUTPUT_CATALOG_FILE = "cso.catalogsource.yaml" |
| 49 | + |
| 50 | +MANIFEST_DIGEST_REGEX = re.compile(r"sha256:[a-z0-9]{64}") |
| 51 | +ARGUMENT_REGEX = re.compile(r"(-[\w])|(--[\w]+)") |
| 52 | +VERSION_REGEX = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$") |
| 53 | +MASTER_VERSION_REGEX = re.compile(r"^master$") |
| 54 | + |
| 55 | + |
| 56 | +def normalize_version(version): |
| 57 | + if VERSION_REGEX.match(version): |
| 58 | + return version[1:] |
| 59 | + return version |
| 60 | + |
| 61 | + |
| 62 | +def get_current_datetime(): |
| 63 | + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 64 | + |
| 65 | + |
| 66 | +def get_image_manifest_digest(image_ref, cmd="docker"): |
| 67 | + """ Return the repo and manifest digest of a given image reference. |
| 68 | + e.g quay.io/namespace/repo:tag -> (quay.io/namespace/repo, sha256:123456) |
| 69 | + """ |
| 70 | + if len(image_ref.split("@")) == 2: |
| 71 | + # Still pull for a digest ref, to make sure the image exists |
| 72 | + repo, digest = image_ref.split("@") |
| 73 | + pull_command = [cmd, "pull", repo+"@"+tag] |
| 74 | + inspect_command = [cmd, "inspect", repo+"@"+tag] |
| 75 | + else: |
| 76 | + repo, tag = image_ref.split(":") |
| 77 | + pull_command = [cmd, "pull", repo+":"+tag] |
| 78 | + inspect_command = [cmd, "inspect", repo+":"+tag] |
| 79 | + |
| 80 | + try: |
| 81 | + subprocess.run(pull_command, check=True) |
| 82 | + out = subprocess.run(inspect_command, check=True, capture_output=True) |
| 83 | + parsed = json.loads(out.stdout) |
| 84 | + repo_digests = parsed[0]["RepoDigests"] |
| 85 | + except subprocess.CalledProcessError as cpe: |
| 86 | + logger.error("Error running docker commands for image %s:%s - %s", repo, tag, cpe) |
| 87 | + return None, None |
| 88 | + except ValueError as ve: |
| 89 | + logger.error("Error parsing docker inspect output output - %s", ve) |
| 90 | + return None, None |
| 91 | + except Exception as e: |
| 92 | + logger.error("Error getting the manifest digest for image %s:%s - %s", repo, tag, e) |
| 93 | + return None, None |
| 94 | + |
| 95 | + repo_digests = list(filter(lambda repo_digest: repo_digest.startswith(repo),repo_digests)) |
| 96 | + if len(repo_digests) == 0: |
| 97 | + logger.error("Could not find the manifest digest for the given image %s:%s", repo, tag) |
| 98 | + return None, None |
| 99 | + |
| 100 | + manifest_digest = repo_digests[0].split("@")[-1] |
| 101 | + if not MANIFEST_DIGEST_REGEX.match(manifest_digest): |
| 102 | + logger.error("Unknown manifest digest format for %s:%s -> %s", repo_digest, manifest_digest) |
| 103 | + return None, None |
| 104 | + |
| 105 | + return repo, manifest_digest |
| 106 | + |
| 107 | + |
| 108 | +def get_b64_logo_from_file(filepath): |
| 109 | + with open(filepath, 'rb') as f: |
| 110 | + data = f.read() |
| 111 | + |
| 112 | + return base64.b64encode(data).decode("ascii") |
| 113 | + |
| 114 | + |
| 115 | +def parse_args(): |
| 116 | + def version_arg_type(arg_value, pat=re.compile(VERSION_REGEX)): |
| 117 | + if not pat.match(arg_value): |
| 118 | + if MASTER_VERSION_REGEX.match(arg_value): |
| 119 | + return arg_value |
| 120 | + |
| 121 | + if not pat.match("v"+arg_value): |
| 122 | + raise argparse.ArgumentTypeError |
| 123 | + |
| 124 | + return "v"+arg_value |
| 125 | + return arg_value |
| 126 | + |
| 127 | + desc = 'Generate CSVs for tagged versions.' |
| 128 | + parser = argparse.ArgumentParser(description=desc) |
| 129 | + parser.add_argument('version', help='Version to generate (SemVer). e.g v1.2.3', type=version_arg_type) |
| 130 | + parser.add_argument('previous_version', help='Previous version.', type=version_arg_type, nargs='?') |
| 131 | + parser.add_argument('--json', dest='yaml', help='Output json config (default).', action='store_false') |
| 132 | + parser.add_argument('--yaml', dest='yaml', help='Output yaml config.', action='store_true') |
| 133 | + parser.add_argument('--upstream', dest='downstream', help='Generate with upstream config.', action='store_false') |
| 134 | + parser.add_argument('--downstream', dest='downstream', help='Generate with downstream config.', action='store_true') |
| 135 | + parser.add_argument('--image', dest='image', help='Image to use in CSV.') |
| 136 | + parser.add_argument('--workdir', dest='workdir', help='Work directory', default=".") |
| 137 | + parser.add_argument('--output-dir', dest='output_dir', help='Output directory relative to the workdir', default="deploy") |
| 138 | + parser.add_argument('--skip-pull', dest='skip_pull', help='Skip pulling the image for verification', action='store_true') |
| 139 | + parser.set_defaults(yaml=True) |
| 140 | + parser.set_defaults(downstream=False) |
| 141 | + parser.set_defaults(previous_version=None) |
| 142 | + parser.set_defaults(skip_pull=False) |
| 143 | + |
| 144 | + logger.debug('Parsing all args') |
| 145 | + _, unknown = parser.parse_known_args() |
| 146 | + |
| 147 | + added_args_keys = set() |
| 148 | + while (len(unknown) > 0 and ARGUMENT_REGEX.match(unknown[0]) and |
| 149 | + ARGUMENT_REGEX.match(unknown[0]).end() == len(unknown[0])): |
| 150 | + logger.info('Adding argument: %s', unknown[0]) |
| 151 | + added_args_keys.add(unknown[0].lstrip('-')) |
| 152 | + parser.add_argument(unknown[0]) |
| 153 | + _, unknown = parser.parse_known_args() |
| 154 | + |
| 155 | + logger.debug('Parsing final set of args') |
| 156 | + return parser.parse_args(), added_args_keys |
| 157 | + |
| 158 | + |
| 159 | +def main(): |
| 160 | + all_args, added_args_keys = parse_args() |
| 161 | + template_kwargs = {key: getattr(all_args, key, None) for key in added_args_keys} |
| 162 | + |
| 163 | + ENV = Environment(loader=FileSystemLoader(os.path.join(all_args.workdir, TEMPLATE_DIR)), undefined=StrictUndefined) |
| 164 | + ENV.filters['normalize_version'] = normalize_version |
| 165 | + ENV.globals['get_current_datetime'] = get_current_datetime |
| 166 | + |
| 167 | + logo = (get_b64_logo_from_file(os.path.join(all_args.workdir, LOGO_DOWNSTREAM_FILE)) |
| 168 | + if all_args.downstream else get_b64_logo_from_file(os.path.join(all_args.workdir,LOGO_UPSTREAM_FILE)) |
| 169 | + ) |
| 170 | + image_ref = all_args.image or CSO_IMAGE + ":" + CSO_IMAGE_TAG |
| 171 | + |
| 172 | + if not all_args.skip_pull: |
| 173 | + repo, image_manifest_digest = get_image_manifest_digest(image_ref) |
| 174 | + if not repo or not image_manifest_digest: |
| 175 | + sys.exit(1) |
| 176 | + |
| 177 | + container_image = repo + "@" + image_manifest_digest |
| 178 | + else: |
| 179 | + container_image = image_ref |
| 180 | + |
| 181 | + template_kwargs["version"] = all_args.version |
| 182 | + template_kwargs["previous_version"] = all_args.previous_version |
| 183 | + template_kwargs["logo"] = logo |
| 184 | + template_kwargs["container_image"] = container_image |
| 185 | + template_kwargs["k8s_api_version"] = template_kwargs.setdefault("k8s_api_version", K8S_API_VERSION) |
| 186 | + |
| 187 | + manifest_output_dir = os.path.join(all_args.output_dir, OUTPUT_MANIFEST_DIR, normalize_version(all_args.version)) |
| 188 | + os.makedirs(manifest_output_dir, exist_ok=True) |
| 189 | + generated_files = {} |
| 190 | + |
| 191 | + assert CSV_TEMPLATE_FILE.endswith(".clusterserviceversion.yaml.jnj") |
| 192 | + csv_template = ENV.get_template(CSV_TEMPLATE_FILE) |
| 193 | + generated_csv = csv_template.render(**template_kwargs) |
| 194 | + csv_filename = CSV_TEMPLATE_FILE.split(".") |
| 195 | + csv_filename.insert(1, all_args.version) |
| 196 | + csv_filename = ".".join(csv_filename[:-1]) |
| 197 | + generated_files[os.path.join(manifest_output_dir, csv_filename)] = generated_csv |
| 198 | + |
| 199 | + for crd_template_file in filter(lambda filename: filename.endswith(".crd.yaml.jnj"), CRD_TEMPLATE_FILES): |
| 200 | + crd_template = ENV.get_template(crd_template_file) |
| 201 | + generated_crd = crd_template.render(**template_kwargs) |
| 202 | + generated_files[os.path.join(manifest_output_dir, crd_template_file.rstrip(".jnj"))] = generated_crd |
| 203 | + |
| 204 | + if all_args.yaml: |
| 205 | + for filepath, content in generated_files.items(): |
| 206 | + with open(filepath, 'w') as f: |
| 207 | + f.write(content) |
| 208 | + else: |
| 209 | + for filepath, content in generated_files.items(): |
| 210 | + parsed = yaml.load(content, Loader=yaml.SafeLoader) |
| 211 | + with open(filepath.rstrip("yaml")+"json", 'w') as f: |
| 212 | + f.write(json.dumps(parsed, default=str)) |
| 213 | + |
| 214 | + |
| 215 | +if __name__ == "__main__": |
| 216 | + main() |
0 commit comments