marketplace/deployer_util/provision.py (645 lines of code) (raw):

#!/usr/bin/env python3 # # Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from argparse import ArgumentParser from make_dns1123_name import dns1123_name, limit_name import yaml import config_helper import log_util as log import property_generator import schema_values_common import storage _PROG_HELP = """ Reads the schemas and writes k8s manifests for objects that need provisioning outside of the deployer to stdout. The manifests include the deployer-related resources. """ _DEFAULT_STORAGE_CLASS_PROVISIONER = 'kubernetes.io/gce-pd' def main(): parser = ArgumentParser(description=_PROG_HELP) schema_values_common.add_to_argument_parser(parser) parser.add_argument('--deployer_image', required=True) parser.add_argument('--deployer_entrypoint', default=None) parser.add_argument('--deployer_service_account_name', required=True) parser.add_argument('--version_repo', default=None) parser.add_argument('--image_pull_secret', default=None) parser.add_argument('--storage_class_provisioner', default=None) args = parser.parse_args() schema = schema_values_common.load_schema(args) values = schema_values_common.load_values(args) manifests = process( schema, values, deployer_image=args.deployer_image, deployer_entrypoint=args.deployer_entrypoint, version_repo=args.version_repo, image_pull_secret=args.image_pull_secret, deployer_service_account_name=args.deployer_service_account_name, storage_class_provisioner=args.storage_class_provisioner) print(yaml.safe_dump_all(manifests, default_flow_style=False, indent=2)) def process(schema, values, deployer_image, deployer_entrypoint, version_repo, image_pull_secret, deployer_service_account_name, storage_class_provisioner): props = {} manifests = [] app_name = get_name(schema, values) namespace = get_namespace(schema, values) # Inject DEPLOYER_IMAGE property values if not already present. values = inject_deployer_image_properties(values, schema, deployer_image) # Handle provisioning of reporting secrets from storage if a URI # is provided. for key, value in values.items(): if key not in schema.properties: continue if not schema.properties[key].reporting_secret: continue if '://' in value: value, storage_manifests = provision_from_storage( key, value, app_name=app_name, namespace=namespace) values[key] = value manifests += storage_manifests for prop in schema.properties.values(): if prop.name in values: # The value has been explicitly specified. Skip. continue if prop.service_account: value, sa_manifests = provision_service_account( schema, prop, app_name=app_name, namespace=namespace, image_pull_secret=image_pull_secret) props[prop.name] = value manifests += sa_manifests elif prop.storage_class: value, sc_manifests = provision_storage_class( schema, prop, app_name=app_name, namespace=namespace, provisioner=storage_class_provisioner) props[prop.name] = value manifests += sc_manifests elif prop.xtype == config_helper.XTYPE_ISTIO_ENABLED: # TODO: Really populate this value. props[prop.name] = False elif prop.xtype == config_helper.XTYPE_INGRESS_AVAILABLE: # TODO(#360): Really populate this value. props[prop.name] = True elif prop.password: props[prop.name] = property_generator.generate_password(prop.password) elif prop.tls_certificate: props[prop.name] = property_generator.generate_tls_certificate() # Merge input and provisioned properties. app_params = dict(list(values.items()) + list(props.items())) use_kalm = False if (schema.is_v2() and schema.x_google_marketplace.managed_updates.kalm_supported): if version_repo: use_kalm = True log.info('Using KALM for deployment') else: log.warn('The deployer supports KALM but no --version-repo specified. ' 'Falling back to provisioning the deployer job only.') if use_kalm: manifests += provision_kalm( schema, version_repo=version_repo, app_name=app_name, namespace=namespace, deployer_image=deployer_image, image_pull_secret=image_pull_secret, app_params=app_params, deployer_service_account_name=deployer_service_account_name) else: manifests += provision_deployer( schema, app_name=app_name, namespace=namespace, deployer_image=deployer_image, deployer_entrypoint=deployer_entrypoint, image_pull_secret=image_pull_secret, app_params=app_params, deployer_service_account_name=deployer_service_account_name) return manifests def inject_deployer_image_properties(values, schema, deployer_image): for key in schema.properties: if key in values: continue if not schema.properties[key].xtype == 'DEPLOYER_IMAGE': continue values[key] = deployer_image return values def provision_from_storage(key, value, app_name, namespace): """Provisions a resource for a property specified from storage.""" raw_manifest = storage.load(value) manifest = yaml.safe_load(raw_manifest) if 'metadata' not in manifest: manifest['metadata'] = {} resource_name = dns1123_name("{}-{}".format(app_name, key)) manifest['metadata']['name'] = resource_name manifest['metadata']['namespace'] = namespace return resource_name, add_preprovisioned_labels([manifest], key) def provision_kalm(schema, version_repo, app_name, namespace, deployer_image, app_params, deployer_service_account_name, image_pull_secret): """Provisions KALM resource for installing the application.""" if not version_repo: raise Exception('A valid --version_repo must be specified') labels = { 'app.kubernetes.io/component': 'kalm.marketplace.cloud.google.com', } secret = make_v2_config(schema, deployer_image, namespace, app_name, labels, app_params) repo = { 'apiVersion': 'kalm.google.com/v1alpha1', 'kind': 'Repository', 'metadata': { 'name': app_name, 'namespace': namespace, 'labels': labels, }, 'spec': { 'type': 'Deployer', 'url': version_repo, }, } release = { 'apiVersion': 'kalm.google.com/v1alpha1', 'kind': 'Release', 'metadata': { 'name': app_name, 'namespace': namespace, 'labels': labels, }, 'spec': { 'repositoryRef': { 'name': app_name, 'namespace': namespace, }, 'version': schema.x_google_marketplace.published_version, 'applicationRef': { 'name': app_name, }, 'serviceAccountName': deployer_service_account_name, 'valuesSecretRef': { 'name': secret['metadata']['name'] } }, } service_account = { 'apiVersion': 'v1', 'kind': 'ServiceAccount', 'metadata': { 'name': deployer_service_account_name, 'namespace': namespace, 'labels': labels, }, } if image_pull_secret: service_account['imagePullSecrets'] = [{ 'name': image_pull_secret, }] role_binding = { 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': '{}-deployer-rb'.format(app_name), 'namespace': namespace, 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': 'cluster-admin', }, 'subjects': [{ 'kind': 'ServiceAccount', 'name': deployer_service_account_name, },] } return [ repo, release, role_binding, secret, service_account, ] def provision_deployer(schema, app_name, namespace, deployer_image, deployer_entrypoint, app_params, deployer_service_account_name, image_pull_secret): """Provisions resources to run the deployer.""" dependents_labels = { 'app.kubernetes.io/component': 'deployer.marketplace.cloud.google.com', 'marketplace.cloud.google.com/deployer': 'Dependent', } dependents_rbac_labels = { 'app.kubernetes.io/component': 'deployer-rbac.marketplace.cloud.google.com', 'marketplace.cloud.google.com/deployer': 'Dependent', } job_labels = { 'app.kubernetes.io/component': 'deployer.marketplace.cloud.google.com', 'marketplace.cloud.google.com/deployer': 'Main', } resources_requests = {'requests': {'memory': '100Mi', 'cpu': '100m'}} if schema.is_v2(): config = make_v2_config(schema, deployer_image, namespace, app_name, dependents_labels, app_params) pod_spec = { 'serviceAccountName': deployer_service_account_name, 'containers': [{ 'name': 'deployer', 'image': deployer_image, 'imagePullPolicy': 'Always', 'volumeMounts': [{ 'name': 'config-volume', 'mountPath': '/data/values.yaml', 'subPath': 'values.yaml', 'readOnly': True, },], 'resources': resources_requests, },], 'restartPolicy': 'Never', 'volumes': [{ 'name': 'config-volume', 'secret': { 'secretName': config['metadata']['name'], }, },] } else: config = make_v1_config(schema, namespace, app_name, dependents_labels, app_params) pod_spec = { 'serviceAccountName': deployer_service_account_name, 'containers': [{ 'name': 'deployer', 'image': deployer_image, 'imagePullPolicy': 'Always', 'volumeMounts': [{ 'name': 'config-volume', 'mountPath': '/data/values', },], 'resources': resources_requests, },], 'restartPolicy': 'Never', 'volumes': [{ 'name': 'config-volume', 'configMap': { 'name': config['metadata']['name'], }, },] } if deployer_entrypoint: pod_spec['containers'][0]['command'] = [deployer_entrypoint] service_account = { 'apiVersion': 'v1', 'kind': 'ServiceAccount', 'metadata': { 'name': deployer_service_account_name, 'namespace': namespace, 'labels': dependents_labels, }, } if image_pull_secret: service_account['imagePullSecrets'] = [{ 'name': image_pull_secret, }] manifests = [ service_account, config, { 'apiVersion': 'batch/v1', 'kind': 'Job', 'metadata': { 'name': "{}-deployer".format(app_name), 'namespace': namespace, 'labels': job_labels, }, 'spec': { 'template': { 'metadata': { 'annotations': { 'sidecar.istio.io/inject': "false", }, }, 'spec': pod_spec, }, 'backoffLimit': 0, }, }, ] manifests += make_deployer_rolebindings(schema, namespace, app_name, dependents_rbac_labels, deployer_service_account_name) return manifests def make_deployer_rolebindings(schema, namespace, app_name, labels, sa_name): subjects = [{ 'kind': 'ServiceAccount', 'name': sa_name, 'namespace': namespace, }] default_rolebinding = { 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': '{}-deployer-rb'.format(app_name), 'namespace': namespace, 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': 'cluster-admin', }, 'subjects': subjects, } if not schema.is_v2( ) or not schema.x_google_marketplace.deployer_service_account: return [default_rolebinding] roles_and_rolebindings = [] deployer_service_account = schema.x_google_marketplace.deployer_service_account # Set the default rolebinding if no namespace roles are defined if not deployer_service_account.custom_role_rules( ) and not deployer_service_account.predefined_roles(): roles_and_rolebindings.append(default_rolebinding) for i, rules in enumerate(deployer_service_account.custom_role_rules()): role_name = '{}-deployer-r{}'.format(app_name, i) roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'Role', 'metadata': { 'name': role_name, 'namespace': namespace, 'labels': labels, }, 'rules': rules, }) roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': '{}-deployer-rb{}'.format(app_name, i), 'namespace': namespace, 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'Role', 'name': role_name, }, 'subjects': subjects, }) for i, rules in enumerate( deployer_service_account.custom_cluster_role_rules()): role_name = '{}:{}:deployer-cr{}'.format(namespace, app_name, i) roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRole', 'metadata': { 'name': role_name, 'labels': labels, }, 'rules': rules, }) roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRoleBinding', 'metadata': { 'name': '{}:{}:deployer-crb{}'.format(namespace, app_name, i), 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': role_name, }, 'subjects': subjects, }) for role in deployer_service_account.predefined_roles(): roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': limit_name('{}:{}-deployer-rb'.format(app_name, role), 64), 'namespace': namespace, 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', # Note: predefined ones are actually cluster roles. 'kind': 'ClusterRole', 'name': role, }, 'subjects': subjects, }) for role in deployer_service_account.predefined_cluster_roles(): roles_and_rolebindings.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRoleBinding', 'metadata': { 'name': limit_name( '{}:{}:{}:deployer-crb'.format(namespace, app_name, role), 64), 'labels': labels, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': role, }, 'subjects': subjects, }) return roles_and_rolebindings def make_v1_config(schema, namespace, app_name, labels, app_params): return { 'apiVersion': 'v1', 'kind': 'ConfigMap', 'metadata': { 'name': '{}-deployer-config'.format(app_name), 'namespace': namespace, 'labels': labels, }, 'data': { k: str(v) for k, v in app_params.items() }, } def make_v2_config(schema, deployer_image, namespace, app_name, labels, app_params): return { 'apiVersion': 'v1', 'kind': 'Secret', 'metadata': { 'name': '{}-deployer-config'.format(app_name), 'namespace': namespace, 'labels': labels, }, 'type': 'Opaque', 'stringData': { 'values.yaml': make_app_params_yaml(app_params, deployer_image), }, } def make_app_params_yaml(app_params, deployer_image): final_app_params = {k: v for k, v in app_params.items()} final_app_params['__image_repo_prefix__'] = deployer_image_to_repo_prefix( deployer_image) return yaml.safe_dump(final_app_params, default_flow_style=False, indent=2) def provision_service_account(schema, prop, app_name, namespace, image_pull_secret): sa_name = dns1123_name('{}-{}'.format(app_name, prop.name)) subjects = [{ 'kind': 'ServiceAccount', 'name': sa_name, 'namespace': namespace, }] service_account = { 'apiVersion': 'v1', 'kind': 'ServiceAccount', 'metadata': { 'name': sa_name, 'namespace': namespace, }, } if image_pull_secret: service_account['imagePullSecrets'] = [{ 'name': image_pull_secret, }] manifests = [service_account] for i, rules in enumerate(prop.service_account.custom_role_rules()): role_name = '{}:{}-r{}'.format(app_name, prop.name, i) manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'Role', 'metadata': { 'name': role_name, 'namespace': namespace, }, 'rules': rules, }) manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': '{}:{}-rb{}'.format(app_name, prop.name, i), 'namespace': namespace, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'Role', 'name': role_name, }, 'subjects': subjects, }) for i, rules in enumerate(prop.service_account.custom_cluster_role_rules()): role_name = '{}:{}:{}-r{}'.format(namespace, app_name, prop.name, i) manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRole', 'metadata': { 'name': role_name, }, 'rules': rules, }) manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRoleBinding', 'metadata': { 'name': '{}:{}:{}-rb{}'.format(namespace, app_name, prop.name, i), 'namespace': namespace, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': role_name, }, 'subjects': subjects, }) for role in prop.service_account.predefined_roles(): manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'RoleBinding', 'metadata': { 'name': limit_name('{}:{}:{}-rb'.format(app_name, prop.name, role), 64), 'namespace': namespace, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', # Note: predefined ones are actually cluster roles. 'kind': 'ClusterRole', 'name': role, }, 'subjects': subjects, }) for role in prop.service_account.predefined_cluster_roles(): manifests.append({ 'apiVersion': 'rbac.authorization.k8s.io/v1', 'kind': 'ClusterRoleBinding', 'metadata': { 'name': limit_name( '{}:{}:{}:{}-crb'.format(namespace, app_name, prop.name, role), 64), 'namespace': namespace, }, 'roleRef': { 'apiGroup': 'rbac.authorization.k8s.io', 'kind': 'ClusterRole', 'name': role, }, 'subjects': subjects, }) return sa_name, add_preprovisioned_labels(manifests, prop.name) def provision_storage_class(schema, prop, app_name, namespace, provisioner): if not provisioner: provisioner = _DEFAULT_STORAGE_CLASS_PROVISIONER volume_binding_mode = 'Immediate' if provisioner == 'kubernetes.io/vsphere-volume': parameters = {'diskformat': 'thin'} elif provisioner == 'kubernetes.io/gce-pd': # WaitForFirstConsumer is only available for gce-pd. See: # https://kubernetes.io/docs/concepts/storage/storage-classes/#volume-binding-mode volume_binding_mode = 'WaitForFirstConsumer' if prop.storage_class.ssd: parameters = { 'type': 'pd-ssd', } else: raise Exception('Do not know how to provision for property {}'.format( prop.name)) elif provisioner == 'kubernetes.io/no-provisioner': # local-shared storage class is already pre-provisioned return 'local-shared', [] sc_name = dns1123_name('{}-{}-{}'.format(namespace, app_name, prop.name)) manifests = [{ 'apiVersion': 'storage.k8s.io/v1', 'kind': 'StorageClass', 'metadata': { 'name': sc_name, }, 'provisioner': provisioner, 'parameters': parameters, 'volumeBindingMode': volume_binding_mode }] return sc_name, add_preprovisioned_labels(manifests, prop.name) def get_name(schema, values): return get_property_value(schema, values, 'NAME') def get_namespace(schema, values): return get_property_value(schema, values, 'NAMESPACE') def get_property_value(schema, values, xtype): candidates = schema.properties_matching({ 'x-google-marketplace': { 'type': xtype, }, }) if len(candidates) != 1: raise Exception('Unable to find exactly one property with ' 'x-google-marketplace.type={}'.format(xtype)) return values[candidates[0].name] def add_preprovisioned_labels(manifests, prop_name): for r in manifests: labels = r['metadata'].get('labels', {}) labels['app.kubernetes.io/component'] = ( 'auto-provisioned.marketplace.cloud.google.com') labels['marketplace.cloud.google.com/auto-provisioned-for-property'] = ( prop_name) r['metadata']['labels'] = labels return manifests def deployer_image_to_repo_prefix(deployer_image): # This strips off the digest or tag at the end of the image name. # All following examples should result in "gcr.io/test/deployer": # - gcr.io/test/deployer # - gcr.io/test/deployer@sha256:abcdef1234567890 # - gcr.io/test/deployer:0.0.0 image_without_tag = deployer_image.split('@')[0].split(':')[0] if not image_without_tag.endswith('/deployer'): raise Exception( 'Deployer image must have "/deployer" as the suffix. Got {}'.format( deployer_image)) return image_without_tag[:-len('/deployer')] if __name__ == '__main__': main()