def guided_install(config, dns_provider, dns_auto_provision, disable_prompt=False): # 01 Verify configuration file exists verify_configuration_file_exists() # 02 Check terraform check_terraform() # 03 Check Environment Variables check_cloud_credentials(config) # 04 Check that oauth settings are set if not disable_prompt: input( 'Ensure that oauth settings are in configuration [Press "Enter" to continue]' ) # 05 Create terraform backend remote state bucket with change_directory("terraform-state"): run(["terraform", "init"]) run(["terraform", "apply", "-auto-approve"]) # 06 Create qhub initial state (up to nginx-ingress) with change_directory("infrastructure"): run(["terraform", "init"]) run( [ "terraform", "apply", "-auto-approve", "-target=module.kubernetes", "-target=module.kubernetes-initialization", "-target=module.kubernetes-ingress", ] ) cmd_output = check_output(["terraform", "output", "--json"]) # This is a bit ugly, but the issue we have at the moment is being unable # to parse cmd_output as json on Github Actions. ip_matches = re.findall(rb'"ip": "(?!string)(.*)"', cmd_output) if ip_matches: ip = ip_matches[0].decode() else: raise ValueError(f"IP Address not found in: {cmd_output}") # 07 Update DNS to point to qhub deployment if dns_auto_provision and dns_provider == "cloudflare": record_name, zone_name = ( config["domain"].split(".")[:-2], config["domain"].split(".")[-2:], ) record_name = f'jupyter.{".".join(record_name)}' zone_name = ".".join(zone_name) if config["provider"] in {"do", "gcp"}: update_record(zone_name, record_name, "A", ip) else: input( f'Take IP Address {ip} and update DNS to point to "jupyter.{config["domain"]}" [Press Enter when Complete]' ) # 08 Full deploy QHub with change_directory("infrastructure"): run(["terraform", "apply", "-auto-approve"])
def add_clearml_dns(zone_name, record_name, record_type, ip_or_hostname): logger.info(f"Setting DNS record for ClearML for record: {record_name}") dns_records = [ f"app.clearml.{record_name}", f"api.clearml.{record_name}", f"files.clearml.{record_name}", ] for dns_record in dns_records: update_record(zone_name, dns_record, record_type, ip_or_hostname)
def deploy_configuration(config): logger.info(f'All qhub endpoints will be under *.{config["domain"]}') jupyterhub_endpoint = f'jupyter.{config["domain"]}' if ("client_id" not in config["authentication"]["config"] or "client_secret" not in config["authentication"]["config"]): logger.info( "client_id and client_secret were not specified - dynamically creating oauth client" ) with timer(logger, "creating oauth client"): config["authentication"]["config"] = auth0.create_client( jupyterhub_endpoint) with timer(logger, "rendering template"): tmp_config = pathlib.Path("./config.yaml") with tmp_config.open("w") as f: yaml.dump(config, f) render_default_template(".", tmp_config) infrastructure_dir = pathlib.Path( config["project_name"]) / "infrastructure" terraform.init(str(infrastructure_dir)) # ========= boostrap infrastructure ======== terraform.apply( str(infrastructure_dir), targets=[ "module.kubernetes", "module.kubernetes-initialization", "module.kubernetes-ingress", ], ) # ============= update dns ================ output = terraform.output(str(infrastructure_dir)) for key in output: if key.startswith("ingress"): endpoint = f'{key.split("_")[1]}.{config["domain"]}' address = output[key]["value"] if re.fullmatch(r"\d+\.\d+\.\d+\.\d+", address): cloudflare.update_record("qhub.dev", endpoint, "A", address) else: cloudflare.update_record("qhub.dev", endpoint, "CNAME", address) # ======= apply entire infrastructure ======== terraform.apply(str(infrastructure_dir))
def guided_install( config, dns_provider, dns_auto_provision, disable_prompt=False, skip_remote_state_provision=False, full_only=False, ): # 01 Check Environment Variables check_cloud_credentials(config) # Check that secrets required for terraform # variables are set as required check_secrets(config) # 02 Create terraform backend remote state bucket # backwards compatible with `qhub-config.yaml` which # don't have `terraform_state` key if ( (not skip_remote_state_provision) and (config.get("terraform_state", {}).get("type", "") == "remote") and (config.get("provider") != "local") ): terraform_state_sync(config) # 3 kubernetes-alpha provider requires that kubernetes be # provisionioned before any "kubernetes_manifests" resources logger.info("Running terraform init") terraform.init(directory="infrastructure") if not full_only: targets = [ "module.kubernetes", "module.kubernetes-initialization", ] logger.info(f"Running Terraform Stage: {targets}") terraform.apply( directory="infrastructure", targets=targets, ) # 04 Create qhub initial state (up to nginx-ingress) targets = ["module.kubernetes-ingress"] logger.info(f"Running Terraform Stage: {targets}") terraform.apply( directory="infrastructure", targets=targets, ) cmd_output = terraform.output(directory="infrastructure") # This is a bit ugly, but the issue we have at the moment is being unable # to parse cmd_output as json on Github Actions. ip_matches = re.findall(r'"ip": "(?!string)(.+)"', cmd_output) hostname_matches = re.findall(r'"hostname": "(?!string)(.+)"', cmd_output) if ip_matches: ip_or_hostname = ip_matches[0] elif hostname_matches: ip_or_hostname = hostname_matches[0] else: raise ValueError(f"IP Address not found in: {cmd_output}") # 05 Update DNS to point to qhub deployment if dns_auto_provision and dns_provider == "cloudflare": record_name, zone_name = ( config["domain"].split(".")[:-2], config["domain"].split(".")[-2:], ) record_name = ".".join(record_name) zone_name = ".".join(zone_name) if config["provider"] in {"do", "gcp", "azure"}: update_record(zone_name, record_name, "A", ip_or_hostname) if config.get("clearml", {}).get("enabled"): add_clearml_dns(zone_name, record_name, "A", ip_or_hostname) elif config["provider"] == "aws": update_record(zone_name, record_name, "CNAME", ip_or_hostname) if config.get("clearml", {}).get("enabled"): add_clearml_dns(zone_name, record_name, "CNAME", ip_or_hostname) else: logger.info( f"Couldn't update the DNS record for cloud provider: {config['provider']}" ) elif not disable_prompt: input( f"Take IP Address {ip_or_hostname} and update DNS to point to " f'"{config["domain"]}" [Press Enter when Complete]' ) # Now Keycloak Helm chart (External Docker Registry before that if we need one) targets = ["module.external-container-reg", "module.kubernetes-keycloak-helm"] logger.info(f"Running Terraform Stage: {targets}") terraform.apply( directory="infrastructure", targets=targets, ) # Now Keycloak realm and config targets = ["module.kubernetes-keycloak-config"] logger.info(f"Running Terraform Stage: {targets}") terraform.apply( directory="infrastructure", targets=targets, ) # Full deploy QHub logger.info("Running Terraform Stage: FULL") terraform.apply(directory="infrastructure")
def guided_install(config, dns_provider, dns_auto_provision, disable_prompt=False): # 01 Verify configuration file exists if not path.exists("qhub-config.yaml"): raise Exception('Configuration file "qhub-config.yaml" does not exist') # 02 Check if Terraform works if which("terraform") is None: raise Exception( f"Please install Terraform with one of the following minor releases: {SUPPORTED_TERRAFORM_MINOR_RELEASES}" ) # 03 Check version of Terraform version_out = check_output(["terraform", "--version"]).decode("utf-8") minor_release = re.search(r"(\d+)\.(\d+)", version_out).group(0) if minor_release not in SUPPORTED_TERRAFORM_MINOR_RELEASES: raise Exception( f"Unsupported Terraform version. Supported minor releases: {SUPPORTED_TERRAFORM_MINOR_RELEASES}" ) # 04 Check Environment Variables if config["provider"] == "gcp": for variable in {"GOOGLE_CREDENTIALS"}: if variable not in os.environ: raise Exception( f"""Missing the following required environment variable: {variable}\n Please see the documentation for more information: {GCP_ENV_DOCS}""" ) elif config["provider"] == "aws": for variable in { "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION", }: if variable not in os.environ: raise Exception( f"""Missing the following required environment variable: {variable}\n Please see the documentation for more information: {AWS_ENV_DOCS}""" ) elif config["provider"] == "do": for variable in { "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "SPACES_ACCESS_KEY_ID", "SPACES_SECRET_ACCESS_KEY", "DIGITALOCEAN_TOKEN", }: if variable not in os.environ: raise Exception( f"""Missing the following required environment variable: {variable}\n Please see the documentation for more information: {DO_ENV_DOCS}""" ) if os.environ["AWS_ACCESS_KEY_ID"] != os.environ[ "SPACES_ACCESS_KEY_ID"]: raise Exception( f"""The environment variables AWS_ACCESS_KEY_ID and SPACES_ACCESS_KEY_ID must be equal\n See {DO_ENV_DOCS} for more information""") if (os.environ["AWS_SECRET_ACCESS_KEY"] != os.environ["SPACES_SECRET_ACCESS_KEY"]): raise Exception( f"""The environment variables AWS_SECRET_ACCESS_KEY and SPACES_SECRET_ACCESS_KEY must be equal\n See {DO_ENV_DOCS} for more information""") else: raise Exception("Cloud Provider configuration not supported") # 05 Check that oauth settings are set if not disable_prompt: input( 'Ensure that oauth settings are in configuration [Press "Enter" to continue]' ) # 06 Create terraform backend remote state bucket with change_directory("terraform-state"): run(["terraform", "init"]) run(["terraform", "apply", "-auto-approve"]) # 07 Create qhub initial state (up to nginx-ingress) with change_directory("infrastructure"): run(["terraform", "init"]) run([ "terraform", "apply", "-auto-approve", "-target=module.kubernetes", "-target=module.kubernetes-initialization", "-target=module.kubernetes-ingress", ]) cmd_output = check_output(["terraform", "output", "--json"]) try: output = json.loads(cmd_output) except json.decoder.JSONDecodeError as e: print(f"Failed to parse terraform output: {cmd_output}") raise e # 08 Update DNS to point to qhub deployment if dns_auto_provision and dns_provider == "cloudflare": record_name, zone_name = ( config["domain"].split(".")[:-2], config["domain"].split(".")[-2:], ) record_name = f'jupyter.{".".join(record_name)}' zone_name = ".".join(zone_name) ip = output["ingress_jupyter"]["value"]["ip"] if config["provider"] in {"do", "gcp"}: update_record(zone_name, record_name, "A", ip) else: input( f'Take IP Address {output["ingress_jupyter"]["value"]["ip"]} and update DNS to point to "jupyter.{config["domain"]}" [Press Enter when Complete]' ) # 09 Full deploy QHub with change_directory("infrastructure"): run(["terraform", "apply", "-auto-approve"])