def generate_delaying_proxy_ingress(concourse_cfg: ConcourseConfig): ensure_not_none(concourse_cfg) proxy_url = concourse_cfg.proxy_url() host = urlparse(proxy_url).netloc tls_secret_name = concourse_cfg.tls_secret_name() return V1beta1Ingress( kind='Ingress', metadata=V1ObjectMeta( name='delaying-proxy', annotations={'kubernetes.io/ingress.class': 'nginx'}, ), spec=V1beta1IngressSpec( rules=[ V1beta1IngressRule( host=host, http=V1beta1HTTPIngressRuleValue(paths=[ V1beta1HTTPIngressPath(backend=V1beta1IngressBackend( service_name='delaying-proxy-svc', service_port=80, ), ), ], ), ), ], tls=[ V1beta1IngressTLS(hosts=[host], secret_name=tls_secret_name), ], ), )
def create_image_pull_secret( credentials: GcrCredentials, image_pull_secret_name: str, namespace: str, ): """Create an image pull secret in the K8s cluster to allow pods to download images from gcr""" ensure_not_none(credentials) ensure_not_empty(image_pull_secret_name) ensure_not_empty(namespace) ctx = kubeutil.ctx namespace_helper = ctx.namespace_helper() namespace_helper.create_if_absent(namespace) secret_helper = ctx.secret_helper() if not secret_helper.get_secret(image_pull_secret_name, namespace): secret_helper.create_gcr_secret( namespace=namespace, name=image_pull_secret_name, password=credentials.passwd(), user_name=credentials.username(), email=credentials.email(), server_url=credentials.host(), ) service_account_helper = ctx.service_account_helper() service_account_helper.patch_image_pull_secret_into_service_account( name="default", namespace=namespace, image_pull_secret_name=image_pull_secret_name)
def deploy_secrets_server(secrets_server_config: SecretsServerConfig): ensure_not_none(secrets_server_config) ctx = kubeutil.ctx service_helper = ctx.service_helper() deployment_helper = ctx.deployment_helper() secrets_helper = ctx.secret_helper() namespace_helper = ctx.namespace_helper() namespace = secrets_server_config.namespace() namespace_helper.create_if_absent(namespace) secret_name = secrets_server_config.secrets().concourse_secret_name() # Deploy an empty secret if none exists so that the secrets-server can start. # However, if there is already a secret we should not purge its contents. if not secrets_helper.get_secret(secret_name, namespace): secrets_helper.put_secret( name=secret_name, data={}, namespace=namespace, ) service = generate_secrets_server_service(secrets_server_config) deployment = generate_secrets_server_deployment(secrets_server_config) service_helper.replace_or_create_service(namespace, service) deployment_helper.replace_or_create_deployment(namespace, deployment)
def create_tls_secret( tls_config: TlsConfig, tls_secret_name: str, namespace: str, ): """Creates the configured TLS secret for the Concourse web-component in the K8s cluster""" ensure_not_none(tls_config) ensure_not_empty(tls_secret_name) ensure_not_empty(namespace) ctx = kubeutil.ctx namespace_helper = ctx.namespace_helper() namespace_helper.create_if_absent(namespace) secret_helper = ctx.secret_helper() if not secret_helper.get_secret(tls_secret_name, namespace): data = { 'tls.key': tls_config.private_key(), 'tls.crt': tls_config.certificate(), } secret_helper.put_secret( name=tls_secret_name, data=data, namespace=namespace, )
def generate_delaying_proxy_deployment(concourse_cfg: ConcourseConfig): ensure_not_none(concourse_cfg) external_url = concourse_cfg.external_url() label = {'app': 'delaying-proxy'} return V1Deployment( kind='Deployment', metadata=V1ObjectMeta(name='delaying-proxy'), spec=V1DeploymentSpec( replicas=1, selector=V1LabelSelector(match_labels=label), template=V1PodTemplateSpec( metadata=V1ObjectMeta(labels=label), spec=V1PodSpec(containers=[ V1Container( image= 'eu.gcr.io/gardener-project/cc/github-enterprise-proxy:0.1.0', image_pull_policy='IfNotPresent', name='delaying-proxy', ports=[ V1ContainerPort(container_port=8080), ], liveness_probe=V1Probe( tcp_socket=V1TCPSocketAction(port=8080), initial_delay_seconds=10, period_seconds=10, ), env=[ V1EnvVar(name='CONCOURSE_URL', value=external_url), ], ), ], ))))
def create(name: str, variant_name: str, args_dict: dict): if not name in TRAITS: raise ModelValidationError('no such trait: ' + str(name)) ensure_not_none(args_dict) ctor = TRAITS[name] return ctor(name=name, variant_name=variant_name, raw_dict=args_dict)
def create_deployment(self, namespace: str, deployment: V1Deployment): '''Create a deployment in a given namespace. Raises an `ApiException` if such a deployment already exists.''' ensure_not_empty(namespace) ensure_not_none(deployment) self.apps_api.create_namespaced_deployment(namespace=namespace, body=deployment)
def create_ingress(self, namespace: str, ingress: V1beta1Ingress): '''Create an ingress in a given namespace. Raises an `ApiException` if such an ingress already exists.''' ensure_not_empty(namespace) ensure_not_none(ingress) self.extensions_v1beta1_api.create_namespaced_ingress( namespace=namespace, body=ingress)
def create_service(self, namespace: str, service: V1Service): '''Create a service in a given namespace. Raises an `ApiException` if such a Service already exists. ''' ensure_not_empty(namespace) ensure_not_none(service) self.core_api.create_namespaced_service(namespace=namespace, body=service)
def set_kubecfg(self, kubeconfig_dict: dict): ensure_not_none(kubeconfig_dict) configuration = kubernetes.client.Configuration() cfg_loader = KubeConfigLoader(dict(kubeconfig_dict)) cfg_loader.load_and_set(configuration) # pylint: disable=no-member kubernetes.client.Configuration.set_default(configuration) # pylint: enable=no-member self.kubeconfig = configuration
def replace_or_create_service(self, namespace: str, service: V1Service): '''Create a service in a given namespace. If the service already exists, the previous version will be deleted beforehand ''' ensure_not_empty(namespace) ensure_not_none(service) service_name = service.metadata.name existing_service = self.get_service(namespace=namespace, name=service_name) if existing_service: self.core_api.delete_namespaced_service(namespace=namespace, name=service_name) self.create_service(namespace=namespace, service=service)
def ensure_cluster_version(kubernetes_config: KubernetesConfig): ensure_not_none(kubernetes_config) cluster_version_info = kubeutil.get_cluster_version_info() configured_version_info = kubernetes_config.cluster_version() if (cluster_version_info.major != configured_version_info['major'] or cluster_version_info.minor != configured_version_info['minor']): fail( 'Incompatible k8s-cluster-version "Major: {a_major} Minor: {a_minor}". Expected "Major: {e_major} Minor: {e_minor}".' .format( a_major=cluster_version_info.major, a_minor=cluster_version_info.minor, e_major=configured_version_info['major'], e_minor=configured_version_info['minor'], ))
def replace_or_create_deployment(self, namespace: str, deployment: V1Deployment): '''Create a deployment in a given namespace. If the deployment already exists, the previous version will be deleted beforehand. ''' ensure_not_empty(namespace) ensure_not_none(deployment) deployment_name = deployment.metadata.name existing_deployment = self.get_deployment(namespace=namespace, name=deployment_name) if existing_deployment: self.apps_api.delete_namespaced_deployment( namespace=namespace, name=deployment_name, body=kubernetes.client.V1DeleteOptions()) self.create_deployment(namespace=namespace, deployment=deployment)
def replace_or_create_ingress(self, namespace: str, ingress: V1beta1Ingress): '''Create an ingress in a given namespace. If the ingress already exists, the previous version will be deleted beforehand. ''' ensure_not_empty(namespace) ensure_not_none(ingress) ingress_name = ingress.metadata.name existing_ingress = self.get_ingress(namespace=namespace, name=ingress_name) if existing_ingress: self.extensions_v1beta1_api.delete_namespaced_ingress( namespace=namespace, name=ingress_name, body=kubernetes.client.V1DeleteOptions()) self.create_ingress(namespace=namespace, ingress=ingress)
def _send_mail( email_cfg: EmailConfig, recipients: typing.Iterable[str], mail_template: str, subject: str, replace_tokens: dict={}, cc_recipients: typing.Iterable[str]=[], ): ensure_not_none(email_cfg) ensure_not_empty(recipients) ensure_not_none(mail_template) ensure_not_empty(subject) # create body from template mail_body = mailer.create_body( mail_template=mail_template, replace_tokens=replace_tokens, ) # create mail envelope mail = mailer.create_mail( subject=subject, sender=email_cfg.sender_name(), recipients=recipients, cc_recipients=cc_recipients, text=mail_body ) if email_cfg.use_tls(): smtp_server = smtplib.SMTP_SSL(email_cfg.smtp_host()) else: smtp_server = smtplib.SMTP(email_cfg.smtp_host()) credentials = email_cfg.credentials() smtp_server.login(user=credentials.username(), password=credentials.passwd()) recipients = set(recipients) recipients.update(cc_recipients) mailer.send_mail( smtp_server=smtp_server, msg=mail, sender=credentials.username(), recipients=recipients )
def set_teams(config: ConcourseConfig): ensure_not_none(config) # Use main-team, i.e. the team that can change the other teams' credentials main_team_credentials = config.main_team_credentials() concourse_api = client.ConcourseApi( base_url=config.external_url(), team_name=main_team_credentials.teamname(), ) concourse_api.login( team=main_team_credentials.teamname(), username=main_team_credentials.username(), passwd=main_team_credentials.passwd(), ) for team in config.all_team_credentials(): # We skip the main team here since we cannot update all its credentials at this time. if team.teamname == "main": continue concourse_api.set_team(team)
def generate_secrets_server_service( secrets_server_config: SecretsServerConfig, ): ensure_not_none(secrets_server_config) # We need to ensure that the labels and selectors match between the deployment and the service, # therefore we base them on the configured service name. service_name = secrets_server_config.service_name() selector = {'app': service_name} return V1Service( kind='Service', metadata=V1ObjectMeta(name=service_name, ), spec=V1ServiceSpec( type='ClusterIP', ports=[ V1ServicePort(protocol='TCP', port=80, target_port=8080), ], selector=selector, session_affinity='None', ), )
def create_instance_specific_helm_values(concourse_cfg: ConcourseConfig): ''' Creates a dict containing instance specific helm values not explicitly stated in the `ConcourseConfig`'s helm_chart_values. ''' ensure_not_none(concourse_cfg) # 'main'-team credentials need to be included in the values.yaml, unlike the other teams creds = concourse_cfg.team_credentials('main') external_url = concourse_cfg.external_url() external_host = urlparse(external_url).netloc concourse_tls_secret_name = concourse_cfg.tls_secret_name() instance_specific_values = { 'concourse': { 'externalURL': external_url, }, 'secrets': { 'basicAuthUsername': creds.username(), 'basicAuthPassword': creds.passwd(), 'githubAuthAuthUrl': creds.github_auth_auth_url(), 'githubAuthTokenUrl': creds.github_auth_token_url(), 'githubAuthApiUrl': creds.github_auth_api_url(), 'githubAuthClientId': creds.github_auth_client_id(), 'githubAuthClientSecret': creds.github_auth_client_secret(), 'githubAuthTeam': creds.github_auth_team(), }, 'web': { 'ingress': { 'hosts': [external_host], 'tls': [{ 'secretName': concourse_tls_secret_name, 'hosts': [external_host], }], } } } return instance_specific_values
def deploy_delaying_proxy( concourse_cfg: ConcourseConfig, deployment_name: str, ): ensure_not_none(concourse_cfg) ensure_not_empty(deployment_name) ctx = kubeutil.ctx service_helper = ctx.service_helper() deployment_helper = ctx.deployment_helper() namespace_helper = ctx.namespace_helper() ingress_helper = ctx.ingress_helper() namespace = deployment_name namespace_helper.create_if_absent(namespace) service = generate_delaying_proxy_service() deployment = generate_delaying_proxy_deployment(concourse_cfg) ingress = generate_delaying_proxy_ingress(concourse_cfg) service_helper.replace_or_create_service(namespace, service) deployment_helper.replace_or_create_deployment(namespace, deployment) ingress_helper.replace_or_create_ingress(namespace, ingress)
def __init__(self, cfg_factory, cfg_name, *args, **kwargs): self.cfg_factory = ensure_not_none(cfg_factory) super().__init__(name=cfg_name, *args, **kwargs) # normalise cfg mappings for cfg_type_name, entry in self.raw.items(): if type(entry) == dict: entry = { 'config_names': entry['config_names'], 'default': entry.get('default', None) } elif type(entry) == str: entry = {'config_names': [entry], 'default': entry} self.raw[cfg_type_name] = entry
def __init__(self, raw_dict: dict): self.raw = ensure_not_none(raw_dict) if not self.CFG_TYPES in self.raw: raise ValueError('missing required attribute: {ct}'.format(ct=self.CFG_TYPES))
def deploy_or_upgrade_concourse( default_helm_values: NamedModelElement, custom_helm_values: NamedModelElement, concourse_cfg: ConcourseConfig, kubernetes_config: KubernetesConfig, deployment_name: str = 'concourse', ): """Deploys (or upgrades) Concourse using the Helm CLI""" ensure_not_none(default_helm_values) ensure_not_none(custom_helm_values) ensure_not_none(concourse_cfg) helm_executable = ensure_helm_setup() namespace = deployment_name # create namespace if absent namespace_helper = kubeutil.ctx.namespace_helper() if not namespace_helper.get_namespace(namespace): namespace_helper.create_namespace(namespace) DEFAULT_HELM_VALUES_FILE_NAME = 'default_helm_values' CUSTOM_HELM_VALUES_FILE_NAME = 'custom_helm_values' INSTANCE_SPECIFIC_HELM_VALUES_FILE_NAME = 'instance_specific_helm_values' KUBECONFIG_FILE_NAME = 'kubecfg' # prepare subprocess args using relative file paths for the values files subprocess_args = [ helm_executable, "upgrade", "--install", "--recreate-pods", "--wait", "--namespace", namespace, # Use Helm's value-rendering mechanism to merge the different value-sources. # This requires one values-file per source, with later value-files taking precedence. "--values", DEFAULT_HELM_VALUES_FILE_NAME, "--values", CUSTOM_HELM_VALUES_FILE_NAME, "--values", INSTANCE_SPECIFIC_HELM_VALUES_FILE_NAME, "--version", CONCOURSE_HELM_CHART_VERSION, namespace, # release name is the same as namespace name "stable/concourse" ] helm_env = os.environ.copy() # set KUBECONFIG env-var in the copy to relative file path helm_env['KUBECONFIG'] = KUBECONFIG_FILE_NAME # create temp dir containing all previously referenced files with tempfile.TemporaryDirectory() as temp_dir: with open(os.path.join(temp_dir, DEFAULT_HELM_VALUES_FILE_NAME), 'w') as f: yaml.dump(default_helm_values, f) with open(os.path.join(temp_dir, CUSTOM_HELM_VALUES_FILE_NAME), 'w') as f: yaml.dump(custom_helm_values, f) with open( os.path.join(temp_dir, INSTANCE_SPECIFIC_HELM_VALUES_FILE_NAME), 'w') as f: yaml.dump( create_instance_specific_helm_values( concourse_cfg=concourse_cfg), f) with open(os.path.join(temp_dir, KUBECONFIG_FILE_NAME), 'w') as f: yaml.dump(kubernetes_config.kubeconfig(), f) # run helm from inside the temporary directory so that the prepared file paths work subprocess.run(subprocess_args, check=True, cwd=temp_dir, env=helm_env)
def generate_secrets_server_deployment( secrets_server_config: SecretsServerConfig, ): ensure_not_none(secrets_server_config) service_name = secrets_server_config.service_name() secret_name = secrets_server_config.secrets().concourse_secret_name() # We need to ensure that the labels and selectors match for both the deployment and the service, # therefore we base them on the configured service name. labels = {'app': service_name} return V1Deployment( kind='Deployment', metadata=V1ObjectMeta(name=service_name, labels=labels), spec=V1DeploymentSpec( replicas=1, selector=V1LabelSelector(match_labels=labels), template=V1PodTemplateSpec( metadata=V1ObjectMeta(labels=labels), spec=V1PodSpec(containers=[ V1Container( image='eu.gcr.io/gardener-project/cc/job-image:0.20.0', image_pull_policy='IfNotPresent', name='secrets-server', resources=V1ResourceRequirements( requests={ 'cpu': '50m', 'memory': '50Mi' }, limits={ 'cpu': '50m', 'memory': '50Mi' }, ), command=['bash'], args=[ '-c', ''' # switch to secrets serving directory (create it if missing, i.e. if no other secrets are mounted there) mkdir -p /secrets && cd /secrets # make Kubernetes serviceaccount secrets available by default cp -r /var/run/secrets/kubernetes.io/serviceaccount serviceaccount # store Kubernetes service endpoint env as file for consumer env | grep KUBERNETES_SERVICE > serviceaccount/env # launch minimalistic python server in that directory serving requests across all network interfaces python3 -m http.server 8080 ''' ], ports=[ V1ContainerPort(container_port=8080), ], liveness_probe=V1Probe( tcp_socket=V1TCPSocketAction(port=8080), initial_delay_seconds=10, period_seconds=10, ), volume_mounts=[ V1VolumeMount( name=secret_name, mount_path='/secrets/concourse-secrets', read_only=True, ), ], ), ], volumes=[ V1Volume(name=secret_name, secret=V1SecretVolumeSource( secret_name=secret_name, )) ]))))
def __init__(self, cfg_sets: 'ConfigurationSet', cfg_factory: ConfigFactory): self.cfg_sets = ensure_not_none(cfg_sets) self.cfg_factory = ensure_not_none(cfg_factory)
def __init__(self, name, raw_dict, *args, **kwargs): self._name = ensure_not_none(name) super().__init__(raw_dict=raw_dict, *args, **kwargs)
def from_dict(raw_dict: dict): raw = ensure_not_none(raw_dict) return ConfigFactory(raw_dict=raw)
def __init__(self, trait: PublishTrait, *args, **kwargs): super().__init__(*args, **kwargs) self.trait = ensure_not_none(trait)
def _preprocess(self): return self.raw.get('preprocess', 'inject-commit-hash') self.args = ensure_not_none(trait_args)
def __init__(self, raw_dict): self.raw = ensure_not_none(raw_dict) self.snd = SimpleNamespaceDict(raw_dict) self._validate_dict()