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")