Exemple #1
0
def delete(
    secret: str,
    env: Optional[str],
    config: str,
    no_restart: bool,
    local: Optional[bool],
    var: Dict[str, str],
    module: Optional[str],
) -> None:
    """Delete a secret key from a k8s service

    Examples:

    opta secret delete -c my-service.yaml "MY_SECRET_1"
    """

    config = check_opta_file_exists(config)
    if local:
        config = local_setup(config, input_variables=var)
        env = "localopta"
    layer = Layer.load_from_yaml(config,
                                 env,
                                 input_variables=var,
                                 strict_input_variables=False)
    secret_name, namespace = get_secret_name_and_namespace(layer, module)

    set_kube_config(layer)
    if check_if_namespace_exists(namespace):
        delete_secret_key(namespace, secret_name, secret)
        __restart_deployments(no_restart, namespace)
    amplitude_client.send_event(amplitude_client.UPDATE_SECRET_EVENT)
    logger.info("Success")
Exemple #2
0
    def delete_remote_state(self) -> None:
        bucket = self.layer.state_storage()
        providers = self.layer.gen_providers(0)
        dynamodb_table = providers["terraform"]["backend"]["s3"][
            "dynamodb_table"]

        self.__get_dynamodb(dynamodb_table).delete_item(
            TableName=dynamodb_table,
            Key={"LockID": {
                "S": f"{bucket}/{self.layer.name}-md5"
            }},
        )

        s3_client = boto3.client("s3", config=Config(region_name=self.region))
        resp = s3_client.delete_object(Bucket=bucket, Key=self.layer.name)

        if resp["ResponseMetadata"]["HTTPStatusCode"] != 204:
            raise Exception(
                f"Failed to delete opta tf state in {bucket}/{self.layer.name}."
            )

        for version in self.get_all_versions(bucket, self.layer.name,
                                             self.region):
            s3_client.delete_object(Bucket=bucket,
                                    Key=self.layer.name,
                                    VersionId=version)
        logger.info(f"Deleted opta tf state for {self.layer.name}")
Exemple #3
0
    def delete_state_storage(cls, layer: "Layer") -> None:
        """
        Idempotently remove remote storage for tf state
        """
        # After the layer is completely deleted, remove the opta config from the state bucket.
        if layer.cloud == "aws":
            cloud_client: CloudClient = AWS(layer)
        elif layer.cloud == "google":
            cloud_client = GCP(layer)
        elif layer.cloud == "azurerm":
            cloud_client = Azure(layer)
        elif layer.cloud == "local":
            cloud_client = Local(layer)
        elif layer.cloud == "helm":
            # There is no opta managed storage to delete
            return
        else:
            raise Exception(
                f"Can not handle opta config deletion for cloud {layer.cloud}")
        cloud_client.delete_opta_config()
        cloud_client.delete_remote_state()

        # If this is the env layer, delete the state bucket & dynamo table as well.
        if layer.name == layer.root().name:

            logger.info(f"Deleting the state storage for {layer.name}...")
            if layer.cloud == "aws":
                cls._aws_delete_state_storage(layer)
            elif layer.cloud == "google":
                cls._gcp_delete_state_storage(layer)
            elif layer.cloud == "local":
                cls._local_delete_state_storage(layer)
Exemple #4
0
def delete_persistent_volume_claims(namespace: str,
                                    opta_managed: bool = True,
                                    async_req: bool = True) -> None:
    """delete_persistent_volume_claims

    Delete Persistent Volume Claims for a given namespace

    This method makes a synchronous HTTP request by default. To make an
    asynchronous HTTP request, please pass async_req=True

    :param str namespace: namespace to search for the Persistent Volume Claims
    :param bool opta_managed: filter to only delete objects managed by opta
    :param bool async_req: execute request asynchronously
    """

    claims = list_persistent_volume_claims(namespace=namespace,
                                           opta_managed=opta_managed)
    if not claims:
        logger.debug(
            f"No persistent volume claim (opta_managed: {opta_managed}) found in namespace '{namespace}', skipping persistent volume cleanup"
        )
        return

    logger.info(f"Deleting persistent volumes in namespace '{namespace}'")

    # delete the PVCs
    # Note: when deleting the PVC, the PV are automatically deleted
    for claim in claims:
        logger.info(
            f"Deleting persistent volume claim '{claim.metadata.name}'")
        delete_persistent_volume_claim(namespace,
                                       claim.metadata.name,
                                       async_req=async_req)
Exemple #5
0
def update(
    secret: str,
    value: str,
    env: Optional[str],
    config: str,
    no_restart: bool,
    local: Optional[bool],
    var: Dict[str, str],
    module: Optional[str],
) -> None:
    """Update a given secret of a k8s service with a new value

    Examples:

    opta secret update -c my-service.yaml "MY_SECRET_1" "value"
    """

    config = check_opta_file_exists(config)
    if local:
        config = local_setup(config, input_variables=var)
        env = "localopta"
    layer = Layer.load_from_yaml(config,
                                 env,
                                 input_variables=var,
                                 strict_input_variables=False)
    secret_name, namespace = get_secret_name_and_namespace(layer, module)

    set_kube_config(layer)
    create_namespace_if_not_exists(namespace)
    amplitude_client.send_event(amplitude_client.UPDATE_SECRET_EVENT)
    update_secrets(namespace, secret_name, {secret: str(value)})
    __restart_deployments(no_restart, namespace)

    logger.info("Success")
Exemple #6
0
    def delete_opta_config(self) -> None:

        if os.path.isfile(self.config_file_path):
            os.remove(self.config_file_path)
            logger.info("Deleted opta config from local")
        else:
            logger.warning(
                f"Did not find opta config {self.config_file_path} to delete")
Exemple #7
0
def gen(
    layer: "Layer",
    existing_config: Optional["StructuredConfig"] = None,
    image_tag: Optional[str] = None,
    image_digest: Optional[str] = None,
    test: bool = False,
    check_image: bool = False,
    auto_approve: bool = False,
) -> Generator[Tuple[int, List["Module"], int], None, None]:
    """Generate TF file based on opta config file"""
    logger.debug("Loading infra blocks")

    total_module_count = len(layer.modules)
    current_modules = []
    for module_idx, module in enumerate(layer.modules):
        logger.debug(f"Generating {module_idx} - {module.name}")
        current_modules.append(module)
        if not module.halt and module_idx + 1 != total_module_count:
            continue
        service_modules = layer.get_module_by_type("k8s-service", module_idx)
        if check_image and len(service_modules) > 0 and cluster_exist(
                layer.root()):
            set_kube_config(layer)

            for service_module in service_modules:
                current_image_info = current_image_digest_tag(layer)
                if (image_digest is None
                        and (current_image_info["tag"] is not None
                             or current_image_info["digest"] is not None)
                        and image_tag is None and service_module.data.get(
                            "image", "").upper() == "AUTO" and not test):
                    if not auto_approve:
                        if click.confirm(
                                f"WARNING There is an existing deployment (tag={current_image_info['tag']}, "
                                f"digest={current_image_info['digest']}) and the pods will be killed as you "
                                f"did not specify an image tag. Would you like to keep the existing deployment alive?",
                        ):
                            image_tag = current_image_info["tag"]
                            image_digest = current_image_info["digest"]
                    else:
                        logger.info(
                            f"{attr('bold')}Using the existing deployment {attr('underlined')}"
                            f"(tag={current_image_info['tag']}, digest={current_image_info['digest']}).{attr(0)}\n"
                            f"{attr('bold')}If you wish to deploy another image, please use "
                            f"{attr('bold')}{attr('underlined')} opta deploy command.{attr(0)}"
                        )
                        image_tag = current_image_info["tag"]
                        image_digest = current_image_info["digest"]
        layer.variables["image_tag"] = image_tag
        layer.variables["image_digest"] = image_digest
        ret = layer.gen_providers(module_idx)
        ret = deep_merge(layer.gen_tf(module_idx, existing_config), ret)

        gen_tf.gen(ret, TF_FILE_PATH)

        yield module_idx, current_modules, total_module_count
Exemple #8
0
 def force_delete_terraform_lock_id(self) -> None:
     logger.info(
         "Trying to Remove the lock forcefully. Will try deleting TF Lock File."
     )
     bucket = self.layer.state_storage()
     tf_lock_path = f"{self.layer.name}/default.tflock"
     credentials, project_id = self.get_credentials()
     gcs_client = storage.Client(project=project_id,
                                 credentials=credentials)
     bucket_object = gcs_client.get_bucket(bucket)
     bucket_object.delete_blob(tf_lock_path)
Exemple #9
0
    def delete_remote_state(self) -> None:

        if os.path.isfile(self.tf_file):
            os.remove(self.tf_file)
            logger.info("Deleted opta tf config from local")
        if os.path.isfile(self.tf_file + ".backup"):
            os.remove(self.tf_file + ".backup")
            logger.info("Deleted opta tf backup config from local")
        else:
            logger.warning(
                f"Did not find opta tf state {self.tf_file} to delete")
Exemple #10
0
def restart_deployments(namespace: str) -> None:
    """restart_deployments

    restart all deployments in the specified namespace, this will honnor the update strategy

    :param str namespace: namespace to search in.
    """
    deployments = list_deployment(namespace)
    for deploy in deployments:
        logger.info(f"Restarting deployment {deploy.metadata.name}")
        restart_deployment(namespace, deploy.metadata.name)
Exemple #11
0
 def delete_opta_config(self) -> None:
     bucket = self.layer.state_storage()
     config_path = f"opta_config/{self.layer.name}"
     credentials, project_id = self.get_credentials()
     gcs_client = storage.Client(project=project_id,
                                 credentials=credentials)
     bucket_object = gcs_client.get_bucket(bucket)
     try:
         bucket_object.delete_blob(config_path)
     except NotFound:
         logger.warning(f"Did not find opta config {config_path} to delete")
     logger.info("Deleted opta config from gcs")
Exemple #12
0
 def delete_remote_state(self) -> None:
     bucket = self.layer.state_storage()
     tfstate_path = f"{self.layer.name}/default.tfstate"
     credentials, project_id = self.get_credentials()
     gcs_client = storage.Client(project=project_id,
                                 credentials=credentials)
     bucket_object = gcs_client.get_bucket(bucket)
     try:
         bucket_object.delete_blob(tfstate_path)
     except NotFound:
         logger.warning(
             f"Did not find opta tf state {tfstate_path} to delete")
     logger.info(f"Deleted opta tf state for {self.layer.name}")
Exemple #13
0
    def delete_bucket(bucket_name: str, region: str) -> None:
        # Before a bucket can be deleted, all of the objects inside must be removed.
        bucket = boto3.resource("s3").Bucket(bucket_name)
        bucket.objects.all().delete()

        # Delete the bucket itself
        logger.info(
            "Sleeping 10 seconds for eventual consistency in deleting all bucket resources"
        )
        sleep(10)
        client = boto3.client("s3", config=Config(region_name=region))
        client.delete_bucket(Bucket=bucket_name)
        print(f"Bucket ({bucket_name}) successfully deleted.")
Exemple #14
0
 def _gcp_delete_state_storage(cls, layer: "Layer") -> None:
     providers = layer.gen_providers(0)
     if "gcs" not in providers.get("terraform", {}).get("backend", {}):
         return
     bucket_name = providers["terraform"]["backend"]["gcs"]["bucket"]
     credentials, project_id = GCP.get_credentials()
     gcs_client = storage.Client(project=project_id,
                                 credentials=credentials)
     try:
         bucket_obj = gcs_client.get_bucket(bucket_name)
         bucket_obj.delete(force=True)
         logger.info("Successfully deleted GCP state storage")
     except NotFound:
         logger.warning("State bucket was already deleted")
Exemple #15
0
def check_version_upgrade(is_upgrade_call: bool = False) -> bool:
    """Logs a warning if newer version of opta is available.

    The version check is not always performed when this function is called.
    It is performed non-deterministically with a probability of UPGRADE_CHECK_PROBABILITY
    in order to not spam the user.
    """
    if OptaUpgrade.successful:
        OptaUpgrade.unset()
        return True
    if is_upgrade_call or _should_check_for_version_upgrade():
        logger.info("Checking for version upgrades...")
        try:
            latest_version = _get_latest_version()
        except Exception as e:
            logger.debug(e, exc_info=True)
            logger.info("Unable to find latest version.")
            return False
        try:
            if semver.VersionInfo.parse(
                    VERSION.strip("v")).compare(latest_version) < 0:
                logger.warning(
                    "New version available.\n"
                    f"You have {VERSION} installed. Latest version is {latest_version}."
                )
                if not is_upgrade_call:
                    print(
                        f"Upgrade instructions are available at {UPGRADE_INSTRUCTIONS_URL}  or simply use the `opta upgrade` command"
                    )
                return True
            else:
                logger.info("User on the latest version.")
        except Exception as e:
            logger.info(f"Semver check failed with error {e}")
    return False
Exemple #16
0
    def force_delete_terraform_lock_id(self) -> None:
        logger.info(
            "Trying to Remove the lock forcefully. Will try deleting Dynamo DB Entry."
        )
        bucket = self.layer.state_storage()
        providers = self.layer.gen_providers(0)
        dynamodb_table = providers["terraform"]["backend"]["s3"][
            "dynamodb_table"]

        self.__get_dynamodb(dynamodb_table).delete_item(
            TableName=dynamodb_table,
            Key={"LockID": {
                "S": f"{bucket}/{self.layer.name}"
            }},
        )
Exemple #17
0
    def _aws_delete_state_storage(cls, layer: "Layer") -> None:
        providers = layer.gen_providers(0)
        if "s3" not in providers.get("terraform", {}).get("backend", {}):
            return

        # Delete the state storage bucket
        bucket_name = providers["terraform"]["backend"]["s3"]["bucket"]
        region = providers["terraform"]["backend"]["s3"]["region"]
        AWS.delete_bucket(bucket_name, region)

        # Delete the dynamodb state lock table
        dynamodb_table = providers["terraform"]["backend"]["s3"][
            "dynamodb_table"]

        AWS.delete_dynamodb_table(dynamodb_table, region)
        logger.info("Successfully deleted AWS state storage")
Exemple #18
0
    def force_unlock(cls, layer: "Layer", *tf_flags: str) -> None:
        tf_lock_exists, lock_id = cls.tf_lock_details(layer)

        if not tf_lock_exists:
            print("Terraform Lock Id could not be found.")
            return

        try:
            nice_run(
                ["terraform", "force-unlock", *tf_flags, lock_id],
                use_asyncio_nice_run=True,
                check=True,
            )
        except Exception as e:
            logger.info(
                "An exception occured while removing the Terraform Lock.")
            cls.force_delete_terraform_lock(layer, e)
Exemple #19
0
    def delete_dynamodb_table(table_name: str, region: str) -> None:
        client = boto3.client("dynamodb", config=Config(region_name=region))

        for _ in range(20):
            try:
                client.delete_table(TableName=table_name)
                print(f"DynamoDB table ({table_name}) successfully deleted.")
                return None
            except client.exceptions.ResourceInUseException:
                logger.info(
                    fmt_msg("""
                        The dynamodb table is currently being created/updated.
                        ~Please wait for deletion to retry..
                    """))
                sleep(5)

        raise Exception("Failed to delete after 20 retries, quitting.")
Exemple #20
0
    def delete_opta_config(self) -> None:
        bucket = self.layer.state_storage()
        config_path = f"opta_config/{self.layer.name}"

        s3_client = boto3.client("s3", config=Config(region_name=self.region))
        resp = s3_client.delete_object(Bucket=bucket, Key=config_path)

        if resp["ResponseMetadata"]["HTTPStatusCode"] != 204:
            raise Exception(
                f"Failed to delete opta config in {bucket}/{config_path}.")

        for version in self.get_all_versions(bucket, config_path, self.region):
            s3_client.delete_object(Bucket=bucket,
                                    Key=config_path,
                                    VersionId=version)

        logger.info("Deleted opta config from s3")
Exemple #21
0
def config(cloud: str) -> None:
    """
    View the opta configuration file for a given cloud provider

    Use the current cloud credentials to fetch the remote opta config file
    """
    if cloud.lower() == "azurerm":
        raise AzureNotImplemented(
            "Currently AzureRM isn't supported for this command, as Opta's AzureRM support is in Beta"
        )
    cloud_client: CloudClient = __get_cloud_client(cloud)
    detailed_config_map = cloud_client.get_all_remote_configs()
    if detailed_config_map:
        for bucket, detailed_configs in detailed_config_map.items():
            for config_name, actual_config in detailed_configs.items():
                logger.info(
                    f"# Bucket Name: {bucket}\n# Config Name: {config_name}\n{actual_config['original_spec']}\n"
                )
Exemple #22
0
def _verify_semver(
    old_semver_string: str,
    current_semver_string: str,
    layer: "Layer",
    auto_approve: bool = False,
) -> None:
    if old_semver_string in [DEV_VERSION, ""] or current_semver_string in [
            DEV_VERSION,
            "",
    ]:
        return

    old_semver = semver.VersionInfo.parse(old_semver_string)
    current_semver = semver.VersionInfo.parse(current_semver_string)
    if old_semver > current_semver:
        logger.warning(
            f"You're trying to run an older version ({current_semver}) of opta (last run with version {old_semver})."
        )
        if not auto_approve:
            click.confirm(
                "Do you wish to upgrade to the latest version of Opta?",
                abort=True,
            )
        _upgrade()
        logger.info("Please rerun the command if the upgrade was successful.")
        exit(0)

    present_modules = [k.aliased_type or k.type for k in layer.modules]

    current_upgrade_warnings = sorted(
        [(k, v) for k, v in UPGRADE_WARNINGS.items()
         if current_semver >= k[0] > old_semver and k[1] == layer.cloud
         and k[2] in present_modules],
        key=lambda x: semver.VersionInfo.parse(x[0][0]),
    )
    for current_upgrade_warning in current_upgrade_warnings:
        logger.info(
            f"{fg('magenta')}WARNING{attr(0)}: Detecting an opta upgrade to or past version {current_upgrade_warning[0]}. "
            f"Got the following warning: {current_upgrade_warning[1]}")
    if not auto_approve and len(current_upgrade_warnings) > 0:
        click.confirm(
            "Are you ok with the aforementioned warnings and done all precautionary steps you wish to do?",
            abort=True,
        )
Exemple #23
0
    def delete_opta_config(self) -> None:
        providers = self.layer.gen_providers(0)
        credentials = self.get_credentials()

        storage_account_name = providers["terraform"]["backend"]["azurerm"][
            "storage_account_name"]
        container_name = providers["terraform"]["backend"]["azurerm"][
            "container_name"]

        storage_client = ContainerClient(
            account_url=f"https://{storage_account_name}.blob.core.windows.net",
            container_name=container_name,
            credential=credentials,
        )
        config_path = f"opta_config/{self.layer.name}"
        try:
            storage_client.delete_blob(config_path, delete_snapshots="include")
        except ResourceNotFoundError:
            logger.info("Remote opta config was already deleted")
Exemple #24
0
def bulk_update(
    env_file: str,
    env: Optional[str],
    config: str,
    no_restart: bool,
    local: Optional[bool],
    var: Dict[str, str],
    module: Optional[str],
) -> None:
    """Bulk update a list of secrets for a k8s service using a dotenv file as in input.

    Each line of the file should be in VAR=VAL format.

    Examples:

    opta secret bulk-update -c my-service.yaml secrets.env
    """

    config = check_opta_file_exists(config)
    if local:
        config = local_setup(config, input_variables=var)
        env = "localopta"
    layer = Layer.load_from_yaml(config,
                                 env,
                                 input_variables=var,
                                 strict_input_variables=False)
    secret_name, namespace = get_secret_name_and_namespace(layer, module)

    set_kube_config(layer)
    create_namespace_if_not_exists(namespace)
    amplitude_client.send_event(amplitude_client.UPDATE_BULK_SECRET_EVENT)

    bulk_update_manual_secrets(namespace, secret_name, env_file)
    __restart_deployments(no_restart, namespace)

    logger.info("Success")
Exemple #25
0
    def display(detailed_plan: bool = False) -> None:
        if detailed_plan:
            regular_plan = Terraform.show(TF_PLAN_PATH, capture_output=True)
            CURRENT_CRASH_REPORTER.tf_plan_text = ansi_scrub(regular_plan
                                                             or "")
            print(regular_plan)
            return
        plan_dict = json.loads(
            Terraform.show(*["-no-color", "-json", TF_PLAN_PATH],
                           capture_output=True)  # type: ignore
        )
        CURRENT_CRASH_REPORTER.tf_plan_text = (
            CURRENT_CRASH_REPORTER.tf_plan_text or json.dumps(plan_dict))
        plan_risk = LOW_RISK
        module_changes: dict = {}
        resource_change: dict
        for resource_change in plan_dict.get("resource_changes", []):
            if resource_change.get("change", {}).get("actions",
                                                     ["no-op"]) == ["no-op"]:
                continue
            address: str = resource_change["address"]

            if not address.startswith("module."):
                logger.warning(
                    f"Unable to determine risk of changes to resource {address}. "
                    "Please run in detailed plan mode for more info")
            module_name = address.split(".")[1]
            module_changes[module_name] = module_changes.get(
                module_name, {
                    "risk": LOW_RISK,
                    "resources": {}
                })
            resource_name = ".".join(address.split(".")[2:])
            actions = resource_change.get("change", {}).get("actions", [])
            if "create" in actions and "delete" in actions:
                actions = ["replace"]
            action = actions[0]
            if action in ["read", "create"]:
                current_risk = LOW_RISK
                action_reason = "data_refresh" if action == "read" else "creation"
            elif action in ["replace", "delete"]:
                current_risk = HIGH_RISK
                action_reason = resource_change.get("action_reason", "N/A")
            elif action in ["update"]:
                current_risk, action_reason = PlanDisplayer.handle_update(
                    resource_change)
            else:
                raise Exception(
                    f"Do not know how to handle planned action: {action}")

            module_changes[module_name]["resources"][resource_name] = {
                "action": action,
                "reason": action_reason,
                "risk": current_risk,
            }
            module_changes[module_name]["risk"] = _max_risk(
                module_changes[module_name]["risk"], current_risk)
            plan_risk = _max_risk(plan_risk, current_risk)

        logger.info(
            f"Identified total risk of {RISK_COLORS[plan_risk]}{plan_risk}{attr(0)}.\n"
            f"{RISK_EXPLANATIONS[plan_risk]}\n"
            "For additional help, please reach out to the RunX team at https://slack.opta.dev/"
        )
        module_changes_list = sorted(
            [(k, v) for k, v in module_changes.items()],
            key=lambda x: x[1]["risk"],
            reverse=True,
        )
        table = []
        for module_name, module_change in module_changes_list:
            resource_changes_list = sorted(
                [(k, v) for k, v in module_change["resources"].items()],
                key=lambda x: x[1]["risk"],
                reverse=True,
            )
            for resource_name, resource_change in resource_changes_list:
                current_risk = resource_change["risk"]
                table.append([
                    f"{fg('blue')}{module_name}{attr(0)}",
                    resource_name,
                    resource_change["action"],
                    f"{RISK_COLORS[current_risk]}{current_risk}{attr(0)}",
                    resource_change["reason"].replace("_", " "),
                ])
        if len(module_changes) == 0:
            logger.info("No changes found.")
        else:
            print(
                tabulate(
                    table,
                    ["module", "resource", "action", "risk", "reason"],
                    tablefmt="fancy_grid",
                ))
        logger.info(
            "For more details, please rerun the command with the --detailed-plan flag."
        )
Exemple #26
0
    def process(self, module_idx: int) -> None:
        if self.layer.is_stateless_mode() is True:
            # do not do create any certificate
            super(AwsDnsProcessor, self).process(module_idx)
            return

        providers = self.layer.gen_providers(0)
        region = providers["provider"]["aws"]["region"]
        self.validate_dns()
        if self.module.data.get("upload_cert"):
            ssm_client: SSMClient = boto3.client(
                "ssm", config=Config(region_name=region))
            parameters = ssm_client.get_parameters_by_path(
                Path=f"/opta-{self.layer.get_env()}",
                Recursive=True).get("Parameters", [])
            parameter_names = list(map(lambda x: x["Name"], parameters))
            files_found = False
            private_key_ssm_path = f"/opta-{self.layer.get_env()}/{PRIVATE_KEY_FILE_NAME}"
            cert_body_ssm_path = (
                f"/opta-{self.layer.get_env()}/{CERTIFICATE_BODY_FILE_NAME}")
            cert_chain_ssm_path = (
                f"/opta-{self.layer.get_env()}/{CERTIFICATE_CHAIN_FILE_NAME}")
            if {private_key_ssm_path,
                    cert_body_ssm_path}.issubset(set(parameter_names)):
                logger.info("SSL files found in cloud")
                files_found = True
            if cert_chain_ssm_path in parameter_names:
                self.module.data["cert_chain_included"] = True
            force_update = self.module.data.get("force_update", False)
            if (force_update or not files_found) and not self.module.data.get(
                    "_updated_already", False):
                logger.info(
                    f"{fg(5)}{attr(1)}You have indicated that you wish to pass in your own ssl certificate and the files have not been "
                    "found on the cloud or you have specified an update must be forced. "
                    "This is not the typically recommended option as the dns delegation way "
                    "includes certificate refreshing so if you don't do this you will need to periodically force a new "
                    f"update. Sometimes this can not be helped, which brings us here.{attr(0)}"
                )
                matching_cert_and_keys = False
                while not matching_cert_and_keys:
                    private_key_obj, private_key_str = self.fetch_private_key()
                    cert_obj, cert_str = self.fetch_cert_body()
                    cert_pub = dump_publickey(FILETYPE_PEM,
                                              cert_obj.get_pubkey())
                    key_pub = dump_publickey(FILETYPE_PEM, private_key_obj)
                    if cert_pub != key_pub:
                        logger.warning(
                            "Certificate private key does not match inputted private key, try again"
                        )
                        continue
                    cert_chain_obj, cert_chain_str = self.fetch_cert_chain()
                    # TODO: add cert chain validation and full chain validation against trusted CA
                    domains_list = self.get_subject_alternative_names(cert_obj)
                    if self.module.data["domain"] not in domains_list:
                        raise UserErrors(
                            f"You provided a domain of {self.module.data['domain']} but the cert is only for domains {domains_list}"
                        )
                    matching_cert_and_keys = True
                if cert_chain_str:
                    ssm_client.put_parameter(
                        Name=cert_chain_ssm_path,
                        Value=cert_chain_str,
                        Type="SecureString",
                        Overwrite=True,
                    )
                    self.module.data["cert_chain_included"] = True
                elif cert_chain_ssm_path in parameter_names:
                    ssm_client.delete_parameter(Name=cert_chain_ssm_path, )
                ssm_client.put_parameter(
                    Name=private_key_ssm_path,
                    Value=private_key_str,
                    Type="SecureString",
                    Overwrite=True,
                )
                ssm_client.put_parameter(
                    Name=cert_body_ssm_path,
                    Value=cert_str,
                    Type="SecureString",
                    Overwrite=True,
                )
                logger.info(
                    "certificate files uploaded securely to parameter store for future consumption"
                )
                self.module.data["_updated_already"] = True
        elif self.module.data.get("external_cert_arn") is not None:
            acm_client: ACMClient = boto3.client(
                "acm", config=Config(region_name=region))
            try:
                cert = acm_client.describe_certificate(CertificateArn=str(
                    self.module.data.get("external_cert_arn")))
            except Exception as e:
                raise UserErrors(
                    f"Encountered error when attempting to verify external certificate {self.module.data.get('external_cert_arn')}: "
                    f"{e}")
            cert_domains = set([cert["Certificate"]["DomainName"]] +
                               cert["Certificate"]["SubjectAlternativeNames"])
            if self.module.data["domain"] not in cert_domains:
                raise UserErrors(
                    f"Inputted certificate is for domains of {cert_domains}, but the main domain "
                    f"{self.module.data['domain']} is not one of them")

        linked_module_name = self.module.data.get("linked_module")
        if linked_module_name is not None:
            x: Module
            linked_modules = list(
                filter(lambda x: linked_module_name in [x.name, x.type],
                       self.layer.modules))
            if len(linked_modules) != 1:
                raise UserErrors(
                    f"Could not find DNS' linked_module of {linked_module_name}-- it must be the name or type of a single module"
                )
        super(AwsDnsProcessor, self).process(module_idx)
Exemple #27
0
def destroy(
    config: str,
    env: Optional[str],
    auto_approve: bool,
    detailed_plan: bool,
    local: Optional[bool],
    var: Dict[str, str],
) -> None:
    """Destroy all opta resources from the current config

    To destroy an environment, you have to first destroy all the services first.

    Examples:

    opta destroy -c my-service.yaml --auto-approve

    opta destroy -c my-env.yaml --auto-approve
    """
    try:
        opta_acquire_lock()
        pre_check()
        logger.warning(
            "You are destroying your cloud infra state. DO NOT, I REPEAT, DO NOT do this as "
            "an attempt to debug a weird/errored apply. What you have created is not some ephemeral object that can be "
            "tossed arbitrarily (perhaps some day) and destroying unnecessarily just to reapply typically makes it "
            "worse. If you're doing this cause you are really trying to destroy the environment entirely, then that's"
            "perfectly fine-- if not then please reach out to the opta team in the slack workspace "
            "(https://slack.opta.dev) and I promise that they'll be happy to help debug."
        )

        config = check_opta_file_exists(config)
        if local:
            config, _ = _handle_local_flag(config, False)
            _clean_tf_folder()
        layer = Layer.load_from_yaml(config, env, input_variables=var)
        event_properties: Dict = layer.get_event_properties()
        amplitude_client.send_event(
            amplitude_client.DESTROY_EVENT, event_properties=event_properties,
        )
        layer.verify_cloud_credentials()
        layer.validate_required_path_dependencies()
        if not Terraform.download_state(layer):
            logger.info(
                "The opta state could not be found. This may happen if destroy ran successfully before."
            )
            return

        tf_lock_exists, _ = Terraform.tf_lock_details(layer)
        if tf_lock_exists:
            raise UserErrors(USER_ERROR_TF_LOCK)

        # Any child layers should be destroyed first before the current layer.
        children_layers = _fetch_children_layers(layer)
        if children_layers:
            # TODO: ideally we can just automatically destroy them but it's
            # complicated...
            logger.error(
                "Found the following services that depend on this environment. Please run `opta destroy` on them first!\n"
                + "\n".join(children_layers)
            )
            raise UserErrors("Dependant services found!")

        tf_flags: List[str] = []
        if auto_approve:
            sleep_time = 5
            logger.info(
                f"{attr('bold')}Opta will now destroy the {attr('underlined')}{layer.name}{attr(0)}"
                f"{attr('bold')} layer.{attr(0)}\n"
                f"{attr('bold')}Sleeping for {attr('underlined')}{sleep_time} secs{attr(0)}"
                f"{attr('bold')}, press Ctrl+C to Abort.{attr(0)}"
            )
            time.sleep(sleep_time)
            tf_flags.append("-auto-approve")
        modules = Terraform.get_existing_modules(layer)
        layer.modules = [x for x in layer.modules if x.name in modules]
        gen_all(layer)
        Terraform.init(False, "-reconfigure", layer=layer)
        Terraform.refresh(layer)

        idx = len(layer.modules) - 1
        for module in reversed(layer.modules):
            try:
                module_address_prefix = f"-target=module.{module.name}"
                logger.info("Planning your changes (might take a minute)")
                Terraform.plan(
                    "-lock=false",
                    "-input=false",
                    "-destroy",
                    f"-out={TF_PLAN_PATH}",
                    layer=layer,
                    *list([module_address_prefix]),
                )
                PlanDisplayer.display(detailed_plan=detailed_plan)
                tf_flags = []
                if not auto_approve:
                    click.confirm(
                        "The above are the planned changes for your opta run. Do you approve?",
                        abort=True,
                    )
                else:
                    tf_flags.append("-auto-approve")
                Terraform.apply(layer, *tf_flags, TF_PLAN_PATH, no_init=True, quiet=False)
                layer.post_delete(idx)
                idx -= 1
            except Exception as e:
                raise e

        Terraform.delete_state_storage(layer)
    finally:
        opta_release_lock()
Exemple #28
0
    def _create_gcp_state_storage(cls, providers: dict) -> None:
        bucket_name = providers["terraform"]["backend"]["gcs"]["bucket"]
        region = providers["provider"]["google"]["region"]
        project_name = providers["provider"]["google"]["project"]
        credentials, project_id = GCP.get_credentials()
        if project_id != project_name:
            raise UserErrors(
                f"We got {project_name} as the project name in opta, but {project_id} in the google credentials"
            )
        gcs_client = storage.Client(project=project_id,
                                    credentials=credentials)
        try:
            bucket = gcs_client.get_bucket(bucket_name)
            bucket_project_number = bucket.project_number
        except GoogleClientError as e:
            if e.code == 403:
                raise UserErrors(
                    f"The Bucket Name: {bucket_name} (Opta needs to store state here) already exists.\n"
                    "Possible Failures:\n"
                    " - Bucket is present in some other project and User does not have access to the Project.\n"
                    "Please change the name in the Opta Configuration file or please change the User Permissions.\n"
                    "Please fix it and try again.")
            elif e.code != 404:
                raise UserErrors(
                    "When trying to determine the status of the state bucket, we got an "
                    f"{e.code} error with the message "
                    f"{e.message}")
            logger.debug(
                "GCS bucket for terraform state not found, creating a new one")
            try:
                bucket = gcs_client.create_bucket(bucket_name, location=region)
                bucket_project_number = bucket.project_number
            except Conflict:
                raise UserErrors(
                    f"It looks like a gcs bucket with the name {bucket_name} was created recently, but then deleted "
                    "and Google keeps hold of gcs bucket names for 30 days after deletion-- pls wait until the end of "
                    "that time or change your environment name slightly.")

        # Enable the APIs
        credentials = GoogleCredentials.get_application_default()
        service = discovery.build("serviceusage",
                                  "v1",
                                  credentials=credentials,
                                  static_discovery=False)
        new_api_enabled = False
        for service_name in [
                "container.googleapis.com",
                "iam.googleapis.com",
                "containerregistry.googleapis.com",
                "cloudkms.googleapis.com",
                "dns.googleapis.com",
                "servicenetworking.googleapis.com",
                "redis.googleapis.com",
                "compute.googleapis.com",
                "secretmanager.googleapis.com",
                "cloudresourcemanager.googleapis.com",
        ]:
            request = service.services().enable(
                name=f"projects/{project_name}/services/{service_name}")
            try:
                response = request.execute()
                new_api_enabled = new_api_enabled or (
                    response.get("name") != "operations/noop.DONE_OPERATION")
            except HttpError as e:
                if e.resp.status == 400:
                    raise UserErrors(
                        f"Got a 400 response when trying to enable the google {service_name} service with the following error reason: {e._get_reason()}"
                    )
            logger.debug(f"Google service {service_name} activated")
        if new_api_enabled:
            logger.info(
                "New api has been enabled, waiting 120 seconds before progressing"
            )
            time.sleep(120)
        service = discovery.build(
            "cloudresourcemanager",
            "v1",
            credentials=credentials,
            static_discovery=False,
        )
        request = service.projects().get(projectId=project_id)
        response = request.execute()

        if response["projectNumber"] != str(bucket_project_number):
            raise UserErrors(
                f"State storage bucket {bucket_name}, has already been created, but it was created in another project. "
                f"Current project's number {response['projectNumber']}. Bucket's project number: {bucket_project_number}. "
                "You do, however, have access to view that bucket, so it sounds like you already run this opta apply in "
                "your org, but on a different project."
                "Note: project number is NOT project id. It is yet another globally unique identifier for your project "
                "I kid you not, go ahead and look it up.")
Exemple #29
0
if __name__ == "__main__":
    try:
        # In case OPTA_DEBUG is set, local state files may not be cleaned up
        # after the command.
        # However, we should still clean them up before the next command, or
        # else it may interfere with it.
        one_time()
        cleanup_files()
        cli()
    except UserErrors as e:
        if os.environ.get("OPTA_DEBUG") is None:
            logger.error(str(e))
        else:
            logger.exception(str(e))
        logger.info(
            f"{fg('magenta')}If you need more help please reach out to the contributors in our slack channel at: https://slack.opta.dev{attr(0)}"
        )
        sys.exit(1)
    except Exception as e:
        logger.exception(str(e))
        logger.info(
            f"{fg('red')}Unhandled error encountered -- a crash report zipfile has been created for you. "
            "If you need more help please reach out (passing the crash report) to the contributors in our "
            f"slack channel at: https://slack.opta.dev{attr(0)}"
            "\nHint: As a first step in debugging, try rerunning the command and seeing if it still fails."
        )
        CURRENT_CRASH_REPORTER.generate_report()
        sys.exit(1)
    finally:
        # NOTE: Statements after the cli() invocation in the try clause are not executed.
        # A quick glance at click documentation did not show why that is the case or any workarounds.
Exemple #30
0
def _apply(
    config: str,
    env: Optional[str],
    refresh: bool,
    local: bool,
    image_tag: Optional[str],
    test: bool,
    auto_approve: bool,
    input_variables: Dict[str, str],
    image_digest: Optional[str] = None,
    stdout_logs: bool = True,
    detailed_plan: bool = False,
) -> None:
    pre_check()
    _clean_tf_folder()
    if local and not test:
        config = local_setup(config,
                             input_variables,
                             image_tag,
                             refresh_local_env=True)

    layer = Layer.load_from_yaml(config, env, input_variables=input_variables)
    layer.verify_cloud_credentials()
    layer.validate_required_path_dependencies()

    if Terraform.download_state(layer):
        tf_lock_exists, _ = Terraform.tf_lock_details(layer)
        if tf_lock_exists:
            raise UserErrors(USER_ERROR_TF_LOCK)
    _verify_parent_layer(layer, auto_approve)

    event_properties: Dict = layer.get_event_properties()
    amplitude_client.send_event(
        amplitude_client.START_GEN_EVENT,
        event_properties=event_properties,
    )

    # We need a region with at least 3 AZs for leader election during failover.
    # Also EKS historically had problems with regions that have fewer than 3 AZs.
    if layer.cloud == "aws":
        providers = layer.gen_providers(0)["provider"]
        aws_region = providers["aws"]["region"]
        azs = _fetch_availability_zones(aws_region)
        if len(azs) < 3:
            raise UserErrors(
                fmt_msg(f"""
                    Opta requires a region with at least *3* availability zones like us-east-1 or us-west-2.
                    ~You configured {aws_region}, which only has the availability zones: {azs}.
                    ~Please choose a different region.
                    """))

    Terraform.create_state_storage(layer)
    gen_opta_resource_tags(layer)
    cloud_client: CloudClient
    if layer.cloud == "aws":
        cloud_client = AWS(layer)
    elif layer.cloud == "google":
        cloud_client = GCP(layer)
    elif layer.cloud == "azurerm":
        cloud_client = Azure(layer)
    elif layer.cloud == "local":
        if local:  # boolean passed via cli
            pass
        cloud_client = Local(layer)
    elif layer.cloud == "helm":
        cloud_client = HelmCloudClient(layer)
    else:
        raise Exception(f"Cannot handle upload config for cloud {layer.cloud}")

    existing_config: Optional[
        StructuredConfig] = cloud_client.get_remote_config()
    old_semver_string = ("" if existing_config is None else
                         existing_config.get("opta_version", "").strip("v"))
    current_semver_string = VERSION.strip("v")
    _verify_semver(old_semver_string, current_semver_string, layer,
                   auto_approve)

    try:
        existing_modules: Set[str] = set()
        first_loop = True
        for module_idx, current_modules, total_block_count in gen(
                layer, existing_config, image_tag, image_digest, test, True,
                auto_approve):
            if first_loop:
                # This is set during the first iteration, since the tf file must exist.
                existing_modules = Terraform.get_existing_modules(layer)
                first_loop = False
            configured_modules = set([x.name for x in current_modules])
            is_last_module = module_idx == total_block_count - 1
            has_new_modules = not configured_modules.issubset(existing_modules)
            if not is_last_module and not has_new_modules and not refresh:
                continue
            if is_last_module:
                untouched_modules = existing_modules - configured_modules
                configured_modules = configured_modules.union(
                    untouched_modules)

            layer.pre_hook(module_idx)
            if layer.cloud == "local":
                if is_last_module:
                    targets = []
            else:
                targets = list(
                    map(lambda x: f"-target=module.{x}",
                        sorted(configured_modules)))
            if test:
                Terraform.plan("-lock=false", *targets, layer=layer)
                print(
                    "Plan ran successfully, not applying since this is a test."
                )
            else:
                current_properties = event_properties.copy()
                current_properties["module_idx"] = module_idx
                amplitude_client.send_event(
                    amplitude_client.APPLY_EVENT,
                    event_properties=current_properties,
                )
                logger.info("Planning your changes (might take a minute)")

                try:
                    Terraform.plan(
                        "-lock=false",
                        "-input=false",
                        f"-out={TF_PLAN_PATH}",
                        layer=layer,
                        *targets,
                        quiet=True,
                    )
                except CalledProcessError as e:
                    logger.error(e.stderr or "")
                    raise e
                PlanDisplayer.display(detailed_plan=detailed_plan)

                if not auto_approve:
                    click.confirm(
                        "The above are the planned changes for your opta run. Do you approve?",
                        abort=True,
                    )
                logger.info("Applying your changes (might take a minute)")
                service_modules = (layer.get_module_by_type(
                    "k8s-service", module_idx) if layer.cloud == "aws" else
                                   layer.get_module_by_type(
                                       "gcp-k8s-service", module_idx))
                if (len(service_modules) != 0 and cluster_exist(layer.root())
                        and stdout_logs):
                    service_module = service_modules[0]
                    # Tailing logs
                    logger.info(
                        f"Identified deployment for kubernetes service module {service_module.name}, tailing logs now."
                    )
                    new_thread = Thread(
                        target=tail_module_log,
                        args=(
                            layer,
                            service_module.name,
                            10,
                            datetime.datetime.utcnow().replace(
                                tzinfo=pytz.UTC),
                            2,
                        ),
                        daemon=True,
                    )
                    # Tailing events
                    new_thread.start()
                    new_thread = Thread(
                        target=tail_namespace_events,
                        args=(
                            layer,
                            datetime.datetime.utcnow().replace(
                                tzinfo=pytz.UTC),
                            3,
                        ),
                        daemon=True,
                    )
                    new_thread.start()

                tf_flags: List[str] = []
                if auto_approve:
                    tf_flags.append("-auto-approve")
                try:
                    Terraform.apply(layer,
                                    *tf_flags,
                                    TF_PLAN_PATH,
                                    no_init=True,
                                    quiet=False)
                except Exception as e:
                    layer.post_hook(module_idx, e)
                    raise e
                else:
                    layer.post_hook(module_idx, None)
                cloud_client.upload_opta_config()
                logger.info("Opta updates complete!")
    except Exception as e:
        event_properties["success"] = False
        event_properties["error_name"] = e.__class__.__name__
        raise e
    else:
        event_properties["success"] = True
    finally:
        amplitude_client.send_event(
            amplitude_client.FINISH_GEN_EVENT,
            event_properties=event_properties,
        )