bot/__main__.py (172 lines of code) (raw):

# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one # or more contributor license agreements. Licensed under the Elastic License; # you may not use this file except in compliance with the Elastic License. import os import json import yaml import click import semver import subprocess import requests from pathlib import Path import assets import packages config = {} def get_stack_version(): es_url = os.getenv("ELASTIC_PACKAGE_ELASTICSEARCH_HOST") es_user = os.getenv("ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME") es_pass = os.getenv("ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD") if not es_url or not es_user or not es_pass: return None res = requests.get(es_url, auth=(es_user, es_pass), verify=False) res.raise_for_status() return res.json()["version"]["number"] def make_plan(branches): tracked_packages = config.get("tracked-packages", {}) local_assets = {} for branch, package, version in assets.walk(): meta = assets.get_meta(branch, package, version) if meta is not None: local_assets.setdefault(package, {}).setdefault(branch, {}).setdefault(version, meta) remote_assets = {} for package in tracked_packages: for branch in tracked_packages[package]["branches"]: package_dir = packages.packages_dir / branch / "packages" / package if package_dir.exists(): for version in os.listdir(package_dir): meta = packages.get_manifest(branch, package, version) if meta is not None: remote_assets.setdefault(package, {}).setdefault(branch, {}).setdefault(version, meta) for package in remote_assets: for branch in remote_assets[package]: local_versions = set(local_assets.get(package, {}).get(branch, {})) remote_versions = set(remote_assets[package][branch]) min_version = tracked_packages[package].get("minimum-version", 0) if min_version: remote_versions = {v for v in remote_versions if v >= min_version or v in local_versions} all_versions = local_versions | remote_versions only_local = local_versions - remote_versions only_remote = remote_versions - local_versions yield (package, branch, all_versions, only_local, only_remote) @click.group() @click.pass_context @click.option("--config", "conf_file", default="config.yaml", show_default=True, help="Path to the configuration file.") def cli(ctx, conf_file): from .config import load try: global config if Path(conf_file).exists(): config = load(conf_file) except Exception as e: click.echo(f"Configuration error: {e}", err=True) ctx.exit(1) @cli.command() @click.pass_context @click.option("--pedantic", is_flag=True, help="Fail if something is wrong with local assets") def meta(ctx, pedantic): """ Print the meta info of all the stored assets """ for branch, package, version in assets.walk(): meta = assets.get_meta(branch, package, version) if meta is None: if pedantic: click.echo(f"Missing or empty meta: assets/{branch}/{package}/{version}/meta.yml", err=True) ctx.exit(1) continue asset = dict(branch=branch, package=package, version=version, meta=meta) click.echo(json.dumps(asset)) @cli.command() @click.option("--branches", help="Comma separated list of branches - es: staging,snapshot") def plan(branches): """ Print the update plan in a diff-like format """ if branches: branches = [b.strip() for b in branches.split(",")] for (package, branch, all_versions, only_local, only_remote) in make_plan(branches): if only_local or only_remote: click.echo(f"--- local/{package}/{branch}") click.echo(f"+++ remote/{package}/{branch}") click.echo(f"@@ -1,{len(only_remote)} +1,{len(only_local)} @@") for version in sorted(all_versions, key=semver.VersionInfo.parse): if version in only_remote: click.echo(f"+{version}") else: click.echo(f" {version}") @cli.command() @click.pass_context @click.option("--branches", help="Comma separated list of branches - es: staging,snapshot") def update(ctx, branches): """ Perform the assets updates """ stack_version = get_stack_version() if not stack_version: click.echo("Forgot to 'eval \"$(elastic-package stack shellinit)\"' in your shell?", err=True) ctx.exit(1) meta = { "stack": { "version": stack_version, }, } if branches: branches = [b.strip() for b in branches.split(",")] for (package, branch, all_versions, only_local, only_remote) in make_plan(branches): for version in sorted(only_remote, key=semver.VersionInfo.parse): package_dir = packages.packages_dir / branch / "packages" / package / version click.echo(f"install package from {package_dir}") args = ["elastic-package", "install", package] p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=package_dir) if p.returncode: click.echo(p.stdout, err=True) click.echo(f"Subprocess returned {p.returncode}", err=True) continue click.echo(p.stdout) asset_dir = assets.assets_dir / branch / package / version click.echo(f"export assets to {asset_dir}") args = ["elastic-package", "dump", "installed-objects", "--package", package, "--output", asset_dir] p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=assets.assets_dir) if p.returncode: click.echo(p.stdout, err=True) click.echo(f"Subprocess returned {p.returncode}", err=True) continue click.echo(p.stdout) click.echo("copy manifest") args = ["cp", "-v", package_dir / "manifest.yml", asset_dir] p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=asset_dir) if p.returncode: click.echo(p.stdout, err=True) click.echo(f"Subprocess returned {p.returncode}", err=True) continue click.echo(p.stdout) click.echo("write meta") with open(asset_dir / "meta.yml", "w+") as f: yaml.dump(meta, f) click.echo(f"git: add {asset_dir}...") args = ["git", "add", "*"] p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=asset_dir) if p.returncode: click.echo(p.stdout, err=True) click.echo(f"Subprocess returned {p.returncode}", err=True) continue click.echo(p.stdout) click.echo(f"git: commit {asset_dir}...") args = ["git", "commit", "-n", "-m", f"Add assets: {package} {version} ({branch}, {stack_version})"] p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=asset_dir) if p.returncode: click.echo(p.stdout, err=True) click.echo(f"Subprocess returned {p.returncode}", err=True) continue click.echo(p.stdout) @cli.command() @click.pass_context @click.argument("PACKAGE") @click.argument("OUTPUT_DIR") def download(ctx, package, output_dir): """ Download the assets of a given package PACKAGE whose assets are to be downloaded - es: endpoint/8.2.3 OUTPUT_DIR directory where the assets are downloaded to """ from github import Github github = Github(os.getenv("GITHUB_TOKEN_ASSETS") or None) repo = github.get_repo("elastic/package-assets") entries = assets.get_remote_assets(package, repo) count = 0 for path, content in assets.download_assets(entries): filename = path.replace(package, output_dir) Path(filename).parent.mkdir(parents=True, exist_ok=True) with open(filename, "wb") as f: f.write(content) count += 1 if count: click.echo(f"Saved {count} assets") else: click.echo(f"Not found: {package}", err=True) ctx.exit(1) if __name__ == "__main__": cli(prog_name="bot")