예제 #1
0
    def get_certification_issuer(self, track: str) -> Optional[str]:
        logger.info(icon=f"{self.ICON} 🏵️️",
                    title="Checking certification issuer",
                    end="")

        raise_exception = False
        if settings.K8S_CLUSTER_ISSUER:
            cert_issuer: str = settings.K8S_CLUSTER_ISSUER
            logger.info(message=" (settings): ", end="")
            raise_exception = True
        else:
            cert_issuer = f"certificate-letsencrypt-{track}"
            logger.info(message=" (track): ", end="")

        os_command = ["kubectl", "get", "clusterissuer", cert_issuer]
        result = run_os_command(os_command, shell=True)
        if not result.return_code:
            logger.success(message=cert_issuer)
            return cert_issuer
        else:
            error_message = f'No issuer "{cert_issuer}" found, using cluster defaults'
            if raise_exception:
                logger.error(message=error_message, raise_exception=True)
            else:
                logger.info(message=error_message)
            return None
예제 #2
0
    def _handle_api_error(error: ApiException,
                          raise_client_exception: bool = False) -> Any:
        """
        Handle a ApiException from the Kubernetes client

        Args:
            error: ApiException to handle
            raise_client_exception: Should the method raise an error on client errors

        Returns:
            The the stringified version of the errors JSON body

        Raises:
            ApiException: If the ``raise_client_exception`` argument is set to ``True``
        """
        error_body = loads_json(error.body)
        if not error_body:
            error_body = {"message": "An unknown error occurred"}
        if Kubernetes._is_client_error(error.status):
            reason = camel_case_split(str(error_body.get("reason", "Unknown")))
            logger.info(
                title=f"{cf.yellow}{cf.bold}{reason}{cf.reset}",
                message=
                f" ({error.status} {cf.italic}{error_body['message'].capitalize()}{cf.reset})",
            )
            if raise_client_exception:
                raise error
        else:
            logger.error(error=error, raise_exception=True)
        return error_body
예제 #3
0
파일: kubernetes.py 프로젝트: Hi-Fi/kolga
    def _parse_file_secrets(
            self,
            filesecrets: Dict[str,
                              str]) -> Tuple[Dict[str, str], Dict[str, str]]:
        if settings.active_ci:
            valid_prefixes = settings.active_ci.VALID_FILE_SECRET_PATH_PREFIXES
        else:
            raise ImproperlyConfigured("An active CI is needed")

        filecontents = {}
        mapping = {}
        for name, filename in filesecrets.items():
            path = Path(filename)
            if not validate_file_secret_path(path, valid_prefixes):
                # TODO: This needs refactoring. Variable names do not match the contents.

                # If path-variable doesn't contain a valid path, we expect it to contain
                # the value for the secret file and we can use it directly.
                logger.warning(
                    f'Not a valid file path for a file "{name}". Using contents as a secret value.'
                )
                # Here we expect that filename-variable contains the secret
                filecontents[name] = b64encode(
                    filename.encode("UTF-8")).decode("UTF-8")
                mapping[name] = f"{settings.K8S_FILE_SECRET_MOUNTPATH}/{name}"
                continue
            try:
                filecontents[name] = self._b64_encode_file(path)
                mapping[name] = f"{settings.K8S_FILE_SECRET_MOUNTPATH}/{name}"
            except OSError as e:
                logger.error(f'Error while reading a file: "{path}"', error=e)

        return filecontents, mapping
예제 #4
0
 def login(
     self,
     ci_jwt: str = settings.VAULT_JWT,
     ci_jwt_private_key: str = settings.VAULT_JWT_PRIVATE_KEY,
 ) -> None:
     if self.initialized:
         try:
             secret_path = f"{settings.PROJECT_NAME}-{self.track}"
             if ci_jwt_private_key:
                 ci_jwt = encode(
                     {
                         "user": secret_path,
                         "aud": secret_path,
                         "exp": datetime.utcnow() + timedelta(seconds=60),
                         "iat": datetime.utcnow(),
                     },
                     ci_jwt_private_key,
                     algorithm="RS256",
                 )
             response = self.client.auth.jwt.jwt_login(
                 role=secret_path,
                 jwt=ci_jwt,
                 path=settings.VAULT_JWT_AUTH_PATH,
             )
             self.client.token = response["auth"]["client_token"]
         except hvac.exceptions.Unauthorized as e:
             logger.error(
                 icon=f"{self.ICON} 🔑",
                 message="Vault login failed!",
                 error=e,
                 raise_exception=True,
             )
예제 #5
0
    def project_deployment_complete(
        self,
        exception: Optional[Exception],
        namespace: str,
        project: "Project",
        track: str,
    ) -> Optional[bool]:
        if not self.configured or exception is not None:
            return None

        deployment_message = new_environment_message(track, project)

        try:
            self.client.chat_postMessage(
                channel=self.SLACK_CHANNEL,
                blocks=deployment_message,
                username="******",
                icon_emoji=":rocket:",
            )
        except SlackApiError as e:
            logger.error(
                message=f"Could not send slack message -> {e.response['error']}"
            )

        return True
예제 #6
0
 def get_chart_name(chart: str) -> str:
     chart_name = chart.split("/")[-1:]
     if not chart_name or not chart_name[0]:
         logger.error(
             message=f"No chart name found in {chart}",
             error=ValueError(),
             raise_exception=True,
         )
     return chart_name[0]
예제 #7
0
 def get_helm_path() -> Path:
     application_path = Path(settings.PROJECT_DIR)
     helm_path = application_path / "helm"
     auto_helm_path = settings.devops_root_path / "helm"
     if not helm_path.exists() and auto_helm_path.exists():
         shutil.copytree(auto_helm_path, helm_path)
     elif not helm_path.exists():
         logger.error(
             message="Could not find Helm chart to use",
             error=OSError(),
             raise_exception=True,
         )
     return helm_path
예제 #8
0
    def _create_basic_auth_data(
        self,
        basic_auth_users: List[BasicAuthUser] = settings.K8S_INGRESS_BASIC_AUTH
    ) -> Dict[str, str]:
        """
        Create secret data from list of `BasicAuthUser`

        The user credentials from the list of users will be encrypted and added
        to a temporary file using the `htpasswd` tool from Apache. The file is
        then read and base64 encoded (as required by Kubernetes secrets).

        Args:
            basic_auth_users: List of `BasicAuthUser`s

        Returns:
            A dict with the key `auth` and base64 content of a htpasswd file as value
        """
        logger.info(icon=f"{self.ICON}  🔨",
                    title="Generating basic auth data: ",
                    end="")

        if not basic_auth_users:
            return {}

        with tempfile.NamedTemporaryFile() as f:
            passwd_path = Path(f.name)
            for i, user in enumerate(basic_auth_users):
                os_command = ["htpasswd", "-b"]
                if i == 0:
                    os_command.append("-c")
                os_command += [str(passwd_path), user.username, user.password]
                result = run_os_command(os_command)
                if result.return_code:
                    logger.error(
                        message=
                        "The 'htpasswd' command failed to create an entry",
                        raise_exception=True,
                    )
            encoded_file = self._b64_encode_file(passwd_path)

        logger.success()
        logger.info(
            message=
            f"\t {len(settings.K8S_INGRESS_BASIC_AUTH)} users will be added to basic auth"
        )

        return {"auth": encoded_file}
예제 #9
0
    def get_secrets(self) -> Dict[str, str]:
        if self.initialized:
            secrets_list = {}
            secret_path = (
                settings.VAULT_PROJECT_SECRET_NAME
                if settings.VAULT_PROJECT_SECRET_NAME
                else f"{settings.PROJECT_NAME}-{self.track}"
            )
            try:
                logger.info(
                    icon=f"{self.ICON} 🔑",
                    message=f"Checking for secrets in {settings.VAULT_KV_SECRET_MOUNT_POINT}/{secret_path}",
                )
                secrets = {}
                if settings.VAULT_KV_VERSION == 2:
                    secrets = self.client.secrets.kv.read_secret_version(
                        path=secret_path,
                        mount_point=settings.VAULT_KV_SECRET_MOUNT_POINT,
                    )
                    secrets_list = secrets["data"]["data"]

                else:
                    secrets = self.client.secrets.kv.v1.read_secret(
                        path=secret_path,
                        mount_point=settings.VAULT_KV_SECRET_MOUNT_POINT,
                    )
                    secrets_list = secrets["data"]

                # Check secrets defined by Terraform
                if settings.VAULT_TF_SECRETS and settings.VAULT_KV_VERSION == 2:
                    secrets_list = self._read_tf_secrets(secret_path, secrets_list)

                # Check for file type secrets
                for key, value in list(secrets_list.items()):
                    if key.startswith(settings.K8S_FILE_SECRET_PREFIX):
                        secrets_list.pop(key)
                        self._create_file_secrets(key, value)

            except hvac.exceptions.InvalidPath as e:
                logger.error(
                    icon=f"{self.ICON} 🔑",
                    message="Secrets not found ",
                    error=e,
                    raise_exception=False,
                )
            return secrets_list
        return {}
예제 #10
0
    def create_client(self, track: str) -> k8s_client.ApiClient:
        try:
            kubeconfig, method = settings.setup_kubeconfig(track)
        except NoClusterConfigError as exc:
            logger.error(
                icon=f"{self.ICON}  🔑",
                message="Can't log in to Kubernetes cluster, all auth methods exhausted",
                error=exc,
                raise_exception=True,
            )

        logger.success(
            icon=f"{self.ICON}  🔑", message=f"Using {method} for Kubernetes auth"
        )

        config = k8s_client.Configuration()
        k8s_config.load_kube_config(client_configuration=config, config_file=kubeconfig)

        return k8s_client.ApiClient(configuration=config)
예제 #11
0
    def _parse_file_secrets(
            self,
            filesecrets: Dict[str,
                              str]) -> Tuple[Dict[str, str], Dict[str, str]]:
        if settings.active_ci:
            valid_prefixes = settings.active_ci.VALID_FILE_SECRET_PATH_PREFIXES
        else:
            raise ImproperlyConfigured("An active CI is needed")

        filecontents = {}
        mapping = {}
        for name, filename in filesecrets.items():
            path = Path(filename)
            if not validate_file_secret_path(path, valid_prefixes):
                logger.warning(f'Not a valid file path: "{path}". Skipping.')
                continue
            try:
                filecontents[name] = self._b64_encode_file(path)
                mapping[name] = f"{settings.K8S_FILE_SECRET_MOUNTPATH}/{name}"
            except OSError as e:
                logger.error(f'Error while reading a file: "{path}"', error=e)

        return filecontents, mapping
예제 #12
0
    def upgrade_chart(
        self,
        name: str,
        values: HelmValues,
        namespace: str,
        chart: str = "",
        chart_path: Optional[Path] = None,
        values_files: Optional[List[Path]] = None,
        install: bool = True,
        version: Optional[str] = None,
        raise_exception: bool = True,
    ) -> SubprocessResult:
        if chart_path:
            if not chart_path.is_absolute():
                chart_path = settings.devops_root_path / chart_path
            if not chart_path.exists():
                logger.error(
                    message=f"Path '{str(chart_path)}' does not exist",
                    error=OSError(),
                    raise_exception=True,
                )
            chart = str(chart_path)

        logger.info(
            icon=f"{self.ICON}  📄",
            title=f"Upgrading chart from '{chart}': ",
            end="",
        )

        replica_timeout_multiplier = 2 if settings.K8S_REPLICACOUNT > 1 else 1
        timeout = (
            (settings.K8S_PROBE_INITIAL_DELAY * replica_timeout_multiplier)
            + (settings.K8S_PROBE_FAILURE_THRESHOLD * settings.K8S_PROBE_PERIOD)
            + 120  # Buffer time
        )

        # Construct initial helm upgrade command
        install_arg = "--install" if install else ""
        helm_command = [
            "helm",
            "upgrade",
            "--atomic",
            "--timeout",
            f"{timeout}s",
            "--history-max",
            "30",
            install_arg,
            "--namespace",
            f"{namespace}",
        ]

        if version:
            helm_command += ["--version", version]

        # Add values files
        if values_files:
            helm_command += self.get_chart_params(flag="--values", values=values_files)

        safe_name = kubernetes_safe_name(name=name)
        values_yaml = yaml.dump(values)

        with NamedTemporaryFile(buffering=0) as fobj:
            fobj.write(values_yaml.encode())
            result = run_os_command(
                [*helm_command, "--values", fobj.name, f"{safe_name}", f"{chart}"],
            )

        if result.return_code:
            logger.std(result, raise_exception=raise_exception)
            return result

        logger.success()
        logger.info(f"\tName: {safe_name} (orig: {name})")
        logger.info(f"\tNamespace: {namespace}")

        return result