diff --git a/config/hubs/farallon.cluster.yaml b/config/hubs/farallon.cluster.yaml new file mode 100644 index 000000000..ab9bf731e --- /dev/null +++ b/config/hubs/farallon.cluster.yaml @@ -0,0 +1,196 @@ +name: farallon +provider: kubeconfig +kubeconfig: + file: secrets/farallon.yaml +hubs: + - name: farallon-staging + domain: staging.farallon.2i2c.cloud + template: daskhub + auth0: + connection: github + config: + scratchBucket: + enabled: false + basehub: + nfsPVC: + nfs: + # from https://docs.aws.amazon.com/efs/latest/ug/mounting-fs-nfs-mount-settings.html + mountOptions: + - rsize=1048576 + - wsize=1048576 + - timeo=600 + - soft # We pick soft over hard, so NFS lockups don't lead to hung processes + - retrans=2 + - noresvport + serverIP: fs-7b129903.efs.us-east-2.amazonaws.com + baseShareName: /homes/ + shareCreator: + tolerations: + - key: node-role.kubernetes.io/master + operator: "Exists" + effect: "NoSchedule" + jupyterhub: + homepage: + templateVars: + org: + name: Farallon Institute + logo_url: https://2i2c.org/media/logo.png + url: http://www.faralloninstitute.org/ + designed_by: + name: 2i2c + url: https://2i2c.org + operated_by: + name: 2i2c + url: https://2i2c.org + funded_by: + name: Farallon Institute + urL: http://www.faralloninstitute.org/ + singleuser: + initContainers: + # Need to explicitly fix ownership here, since EFS doesn't do anonuid + - name: volume-mount-ownership-fix + image: busybox + command: ["sh", "-c", "id && chown 1000:1000 /home/jovyan && ls -lhd /home/jovyan"] + securityContext: + runAsUser: 0 + volumeMounts: + - name: home + mountPath: /home/jovyan + subPath: "{username}" + image: + name: 677861182063.dkr.ecr.us-east-2.amazonaws.com/2i2c-hub/user-image + tag: 9cd76f1 + profileList: + # The mem-guarantees are here so k8s doesn't schedule other pods + # on these nodes. + - display_name: "Default: m5.xlarge" + description: "~4CPUs & ~15GB RAM" + kubespawner_override: + # Expllicitly unset mem_limit, so it overrides the default memory limit we set in + # basehub/values.yaml + mem_limit: null + mem_guarantee: 14G + cpu_guarantee: 3 + node_selector: + hub.jupyter.org/pool-name: notebook-m5-xlarge + - display_name: "Default: m5.2xlarge" + description: "~8CPUs & ~30GB RAM" + kubespawner_override: + # Expllicitly unset mem_limit, so it overrides the default memory limit we set in + # basehub/values.yaml + mem_limit: null + mem_guarantee: 28G + cpu_guarantee: 7 + node_selector: + hub.jupyter.org/pool-name: notebook-m5-2xlarge + scheduling: + userPlaceholder: + enabled: false + replicas: 0 + userScheduler: + enabled: false + proxy: + service: + type: LoadBalancer + https: + enabled: true + hosts: + - staging.farallon.2i2c.cloud + chp: + nodeSelector: {} + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + traefik: + nodeSelector: {} + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + hub: + allowNamedServers: true + networkPolicy: + # FIXME: For dask gateway + enabled: false + readinessProbe: + enabled: false + nodeSelector: {} + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + dask-gateway: + traefik: + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + controller: + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + gateway: + tolerations: + - key: "node-role.kubernetes.io/master" + effect: "NoSchedule" + backend: + scheduler: + extraPodConfig: + nodeSelector: + hub.jupyter.org/pool-name: dask-worker + tolerations: + - key: "k8s.dask.org/dedicated" + operator: "Equal" + value: "worker" + effect: "NoSchedule" + - key: "k8s.dask.org_dedicated" + operator: "Equal" + value: "worker" + effect: "NoSchedule" + worker: + extraPodConfig: + nodeSelector: + hub.jupyter.org/pool-name: dask-worker + tolerations: + - key: "k8s.dask.org/dedicated" + operator: "Equal" + value: "worker" + effect: "NoSchedule" + - key: "k8s.dask.org_dedicated" + operator: "Equal" + value: "worker" + effect: "NoSchedule" + + # TODO: figure out a replacement for userLimits. + extraConfig: + optionHandler: | + from dask_gateway_server.options import Options, Integer, Float, String + def cluster_options(user): + def option_handler(options): + if ":" not in options.image: + raise ValueError("When specifying an image you must also provide a tag") + extra_annotations = { + "hub.jupyter.org/username": user.name, + "prometheus.io/scrape": "true", + "prometheus.io/port": "8787", + } + extra_labels = { + "hub.jupyter.org/username": user.name, + } + return { + "worker_cores_limit": options.worker_cores, + "worker_cores": min(options.worker_cores / 2, 1), + "worker_memory": "%fG" % options.worker_memory, + "image": options.image, + "scheduler_extra_pod_annotations": extra_annotations, + "worker_extra_pod_annotations": extra_annotations, + "scheduler_extra_pod_labels": extra_labels, + "worker_extra_pod_labels": extra_labels, + } + return Options( + Integer("worker_cores", 2, min=1, max=16, label="Worker Cores"), + Float("worker_memory", 4, min=1, max=32, label="Worker Memory (GiB)"), + String("image", default="pangeo/pangeo-notebook:latest", label="Image"), + handler=option_handler, + ) + c.Backend.cluster_options = cluster_options + idle: | + # timeout after 30 minutes of inactivity + c.KubeClusterConfig.idle_timeout = 1800 \ No newline at end of file diff --git a/config/hubs/schema.yaml b/config/hubs/schema.yaml index f7ca33133..ea2e2fd42 100644 --- a/config/hubs/schema.yaml +++ b/config/hubs/schema.yaml @@ -14,9 +14,23 @@ properties: type: string description: | Cloud provider this cluster is running on. Used to perform - authentication against the cluster. Currently supports gcp. + authentication against the cluster. Currently supports gcp + and raw kubeconfig files. enum: - gcp + - kubeconfig + kubeconfig: + type: object + description: | + Configuration to connect to a cluster purely via a kubeconfig + file. + additionalProperties: false + properties: + file: + type: string + descriptiON: | + Path to kubeconfig file (encrypted with sops) to use for + connecting to the cluster gcp: type: object additionalProperties: false diff --git a/deployer/hub.py b/deployer/hub.py index 40e18bc2c..2670ec298 100644 --- a/deployer/hub.py +++ b/deployer/hub.py @@ -37,12 +37,30 @@ def build_image(self): @contextmanager def auth(self): - with tempfile.NamedTemporaryFile() as kubeconfig: - # FIXME: This is dumb - os.environ['KUBECONFIG'] = kubeconfig.name - assert self.spec['provider'] == 'gcp' - + if self.spec['provider'] == 'gcp': yield from self.auth_gcp() + elif self.spec['provider'] == 'kubeconfig': + yield from self.auth_kubeconfig() + else: + raise ValueError(f'Provider {self.spec["provider"]} not supported') + + + def auth_kubeconfig(self): + """ + Context manager for authenticating with just a kubeconfig file + + For the duration of the contextmanager, we: + 1. Decrypt the file specified in kubeconfig.file with sops + 2. Set `KUBECONFIG` env var to our decrypted file path, so applications + we call (primarily helm) will use that as config + """ + config = self.spec['kubeconfig'] + config_path = config['file'] + + with decrypt_file(config_path) as decrypted_key_path: + # FIXME: Unset this after our yield + os.environ['KUBECONFIG'] = decrypted_key_path + yield def auth_gcp(self): config = self.spec['gcp'] @@ -52,23 +70,23 @@ def auth_gcp(self): # Else, it'll just have a `zone` key set. Let's respect either. location = config.get('zone', config.get('region')) cluster = config['cluster'] + with tempfile.NamedTemporaryFile() as kubeconfig: + with decrypt_file(key_path) as decrypted_key_path: + subprocess.check_call([ + 'gcloud', 'auth', + 'activate-service-account', + '--key-file', os.path.abspath(decrypted_key_path) + ]) - with decrypt_file(key_path) as decrypted_key_path: subprocess.check_call([ - 'gcloud', 'auth', - 'activate-service-account', - '--key-file', os.path.abspath(decrypted_key_path) + 'gcloud', 'container', 'clusters', + # --zone works with regions too + f'--zone={location}', + f'--project={project}', + 'get-credentials', cluster ]) - subprocess.check_call([ - 'gcloud', 'container', 'clusters', - # --zone works with regions too - f'--zone={location}', - f'--project={project}', - 'get-credentials', cluster - ]) - - yield + yield class Hub: diff --git a/hub-templates/basehub/templates/cloud-resources/gcp/env-vars.yaml b/hub-templates/basehub/templates/cloud-resources/gcp/env-vars.yaml deleted file mode 100644 index 442e8c23f..000000000 --- a/hub-templates/basehub/templates/cloud-resources/gcp/env-vars.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{ if .Values.jupyterhub.cloudResources.scratchBucket.enabled}} -kind: ConfigMap -apiVersion: v1 -metadata: - name: cloud-env-vars -data: - scratch-bucket-name: {{ include "cloudResources.scratchBucket.name" . }} - scratch-bucket-protocol: "gcs" -{{- end }} \ No newline at end of file diff --git a/hub-templates/basehub/templates/cloud-resources/gcp/service-account.yaml b/hub-templates/basehub/templates/cloud-resources/gcp/service-account.yaml index 9c25341fb..1023c192c 100644 --- a/hub-templates/basehub/templates/cloud-resources/gcp/service-account.yaml +++ b/hub-templates/basehub/templates/cloud-resources/gcp/service-account.yaml @@ -37,11 +37,4 @@ spec: apiVersion: resourcemanager.cnrm.cloud.google.com/v1beta1 kind: Project external: projects/{{ .Values.jupyterhub.cloudResources.gcp.projectId }} ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - annotations: - iam.gke.io/gcp-service-account: {{ include "cloudResources.gcp.serviceAccountName" .}}@{{ .Values.jupyterhub.cloudResources.gcp.projectId }}.iam.gserviceaccount.com - name: user-sa {{- end }} \ No newline at end of file diff --git a/hub-templates/basehub/templates/nfs-pvc.yaml b/hub-templates/basehub/templates/nfs-pvc.yaml index cb6b1ab61..61c7b02e9 100644 --- a/hub-templates/basehub/templates/nfs-pvc.yaml +++ b/hub-templates/basehub/templates/nfs-pvc.yaml @@ -11,10 +11,7 @@ spec: nfs: server: {{ .Values.nfsPVC.nfs.serverIP | quote}} path: "{{ .Values.nfsPVC.nfs.baseShareName }}{{ .Release.Name }}" - mountOptions: - - soft - - noatime - - vers=4.2 + mountOptions: {{ .Values.nfsPVC.nfs.mountOptions | toJson }} --- apiVersion: v1 kind: PersistentVolumeClaim diff --git a/hub-templates/basehub/templates/nfs-share-creator.yaml b/hub-templates/basehub/templates/nfs-share-creator.yaml index e5522ddf1..c3333ee80 100644 --- a/hub-templates/basehub/templates/nfs-share-creator.yaml +++ b/hub-templates/basehub/templates/nfs-share-creator.yaml @@ -22,6 +22,8 @@ spec: spec: restartPolicy: Never terminationGracePeriodSeconds: 0 + tolerations: {{ .Values.nfsPVC.shareCreator.tolerations | toJson }} + containers: - name: dummy image: busybox diff --git a/hub-templates/basehub/templates/user-sa.yaml b/hub-templates/basehub/templates/user-sa.yaml new file mode 100644 index 000000000..102c25657 --- /dev/null +++ b/hub-templates/basehub/templates/user-sa.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + {{ if .Values.jupyterhub.cloudResources.scratchBucket.enabled}} + {{ if eq .Values.jupyterhub.cloudResources.provider "gcp" }} + iam.gke.io/gcp-service-account: {{ include "cloudResources.gcp.serviceAccountName" .}}@{{ .Values.jupyterhub.cloudResources.gcp.projectId }}.iam.gserviceaccount.com + {{- end }} + {{- end }} + name: user-sa \ No newline at end of file diff --git a/hub-templates/basehub/values.yaml b/hub-templates/basehub/values.yaml index 755f81c7d..b57d72628 100644 --- a/hub-templates/basehub/values.yaml +++ b/hub-templates/basehub/values.yaml @@ -19,6 +19,10 @@ nfsPVC: shareCreator: tolerations: [] nfs: + mountOptions: + - soft + - noatime + - vers=4.2 serverIP: nfs-server-01 # MUST HAVE TRAILING SLASH baseShareName: /export/home-01/homes/ diff --git a/hub-templates/daskhub/values.yaml b/hub-templates/daskhub/values.yaml index 1ab216ffc..cef9db1a8 100644 --- a/hub-templates/daskhub/values.yaml +++ b/hub-templates/daskhub/values.yaml @@ -52,30 +52,6 @@ basehub: # The default worker image matches the singleuser image. DASK_GATEWAY__CLUSTER__OPTIONS__IMAGE: '{JUPYTER_IMAGE_SPEC}' - # FIXME: Only set these if scratchBucket.enabled is true - # Explicitly order environment variables that depend on each - # other, since a environment variable needs to be defined first - # before they can be interpolated. - # See https://github.com/jupyterhub/kubespawner/issues/491 - daskhub-01-scratch-bucket-protocol: - name: SCRATCH_BUCKET_PROTOCOL - valueFrom: - configMapKeyRef: - name: cloud-env-vars - key: scratch-bucket-protocol - daskhub-02-scratch-bucket-name: - name: SCRATCH_BUCKET_NAME - valueFrom: - configMapKeyRef: - name: cloud-env-vars - key: scratch-bucket-name - daskhub-03-scratch-bucket: - name: SCRATCH_BUCKET - value: $(SCRATCH_BUCKET_PROTOCOL)://$(SCRATCH_BUCKET_NAME)/$(JUPYTERHUB_USER) - daskhub-04-pangeo-scratch: - name: PANGEO_SCRATCH - value: $(SCRATCH_BUCKET) - hub: networkPolicy: enabled: false @@ -116,6 +92,30 @@ basehub: break else: print("dask-gateway service not found. Did you set jupyterhub.hub.services.dask-gateway.apiToken?") + daskhub-02-cloud-storage-bucket: | + from z2jh import get_config + cloud_resources = get_config('cloudResources') + scratch_bucket = cloud_resources['scratchBucket'] + import os + + if scratch_bucket['enabled']: + # FIXME: Support other providers too + assert cloud_resources['provider'] == 'gcp' + project_id = cloud_resources['gcp']['projectId'] + + release = os.environ['HELM_RELEASE_NAME'] + bucket_protocol = 'gcs' + bucket_name = f'{project_id}-{release}-scratch-bucket' + env = { + 'SCRATCH_BUCKET_PROTOCOL': bucket_protocol, + # Matches "daskhub.scratchBUcket.name" helm template + 'SCRATCH_BUCKET_NAME': bucket_name, + # Use k8s syntax of $(ENV_VAR) to substitute env vars dynamically in other env vars + 'SCRATCH_BUCKET': f'{bucket_protocol}://{bucket_name}/$(JUPYTERHUB_USER)', + 'PANGEO_SCRATCH': f'{bucket_protocol}://{bucket_name}/$(JUPYTERHUB_USER)', + } + + c.KubeSpawner.environment.update(env) dask-gateway: enabled: true # Enabling dask-gateway will install Dask Gateway as a dependency. diff --git a/secrets/farallon.yaml b/secrets/farallon.yaml new file mode 100644 index 000000000..0c89d5c39 --- /dev/null +++ b/secrets/farallon.yaml @@ -0,0 +1,33 @@ +apiVersion: ENC[AES256_GCM,data:6ZE=,iv:uSEgAdks5fDnloUx5WMAewuhNi5T+4MBek+E8OxwxfU=,tag:40Q3Fb5NLXiBlpua5yCGHg==,type:str] +clusters: + - cluster: + certificate-authority-data: ENC[AES256_GCM,data:AylHh/ZF+an80mTUhqeE2vpDS2ALNO2N0CxXlmHPCGpBYLAH7Bq0FY1fM0EXbzip9VLV+AxcBQ8SS3IsmuGQcNpn2CBkmbOL24m9yBqAOACZ4zKUDANtkzTILPyUkwbmOYZgPxptSC/n3IUw6YuyJcTlkTuLit9Vsoz7GLLC7B0zQ/sg0DchGeKINMiD36L2tmdBbIHJZFa6ZgZiLVu7LAGI+zc9QtnkuMYsrrhw+1XQnVhQCpl/a/8vEGoO8qAXx6rPSIk89Y0XpKqn0+iASamOb0F7eKtQZDXNf3fJzQ7pBYmj3h3XfRySkkLEvDC8WH1DjMNsY0shZFxUpszxGkruodDbZard91TU3JS5B700SFd22G1wp4r81hSEg6KwBLM91P/zpgaTE5l2tOdVRkX7Dui15sPPRiutyRIIFSUCF8kXukbyzRFC9T0jjcMCh2+046Vn6EG1bzx3zdXXayeODo4YW6B6wEpIjReieVb+kx74yIn9EJgKz1PiYMh4Va2e0LiX4629IJ6uwlrzFtAeLcjWhsPJv1gT5n1I8Y443PSEAlGqdRsrHuf0LKVPkdT75NzQ0iFQ0EyRium2ikkjHH3YvFqStngy8odgAddwIw3wzrLz0QTCC8yg4sHUpqQVFH+Ls2/oga4bWg/VuuC3JIT/XPXO0qr0kO9TUn18e2tvXbXdo/D+sZZh621kSTvWSMKEZcXRgDQutIavnKeaFjSIAn3hzmXXteZfXsxnOeTSzDb9M6CNe4MVR6PyOrKzXuCCa/IOQ1MJn7ET0Q4zimjEkrkNzm/grjFYavKey++rvOcvRRBTswv7iaPjoR7cqh3oRXhkr0lHgrvu2R3d0LgE/EDsRtV7R0YoD3UGXyWm5y9pVOuzgKQXYEiuSOaUnuUVq5D1xcF9H79O5aeERXnJJqCd+LIr3dcm7iAZMipVjAzgv/Sf8vNusG7kQHAtMt2olug2Jpxq+3KLDyrdFwEU+x2FOpPX1M4TMQZO5wUtP5f72XJ6nc1FQYjPGgP5HpGSKkoh0Rs/kLqrnv/0OgQ2TtggNZGxBhWdmoeh7BA189xb7iTKb8npeGfg2JanOVD6WRLXcT4B4sRIk6DXPSIsVfKyeoSaIt7i9Ponk/K4MK6KeV0WV+XdNpDEUe/frdfcofXfl/vz1qfzKBaV12GECFwlKOqLRKfU6mXqBqM2XBge5qyYiMT+BlKcvXtI4A04n2M56jpRqaaG+55f5G/zI/+ze/jaUMfpoDb5nqnlAgcEIPHRNnR0ns9Ots02GDxOK0Vh+cbgGqhfRefgLBYuQ8OY20NljcdgZlta37qg9jF4qmfk7q2wn3RAYYk5YRq9T9kBhFWx4RiaCOhyuzxYBXI7JxMuGWhopvi75SRlD2QtLylSNEhXsDaJ1aUdsiuG94vkLV6VGogg7pshEBi4FGbEZDMr3eYN1+q7SSAYAp1b5/p4j2l0qvpUaW6wNBG0MWGtptgSgIyNEWqB89agQaKqEY4vvZw7QdXk4HqrIo2VwBmxF6te2FYQxli4+JUFsmN83jVWiQWJM+eMqmc+r+0iMG1BQVc2Nsc4Btz85+yJOS9XGRQCyfrK4UNvvLTo7AMiaTRjj3oDA8vazESXB/jYh+7ZVPusjaysLGvgYNWdsD8BVajl3f+z7ActnJNuyXrpzFvnYd8glHRrzPXtJxXPu3W+cWubtMUQfOtPix3zJYuKsquFwS6vaVysaD4e9/mKjMKVH1dEkfRiawsu07dnnZ5YCZ/Y9xdN6tklLnMHh2iEJMwbjzmLBO+MU9U8ETqvjwaukfeWNny5SnzBTw95mmxhUhaNeKgrtWe91H6kxGt/TlCWOzLragcKGvau8WyPet6rKGrE9r4WGmOibt2GC+yqZ9ZzRArqRYQ/696GFa45xl+6K8pUK+LCEg==,iv:pbWOYLHhJAInwkHqwGC48Im4xdyRWcsG5i+XvO2lcik=,tag:tc1lwgAqD3oPZa7uB1gang==,type:str] + server: ENC[AES256_GCM,data:S2b7TDewzyCMpuEgBO+lwODqaBPZotF0I7gzMOf18cSG9mjkz6ojs9Vk8jK5fwMZME4tSFjwC+yI+UwnQak/m9tZasZfOEK0FuN1lRQ54A==,iv:hyzG7zKNeVdJp13UZaH/sBTQXe9PuSykgCKfI0DkyCY=,tag:fGf/MhbkIk2aT4UKquh+5Q==,type:str] + name: ENC[AES256_GCM,data:6MDBvzbHDKERzFlLWi9AnOkeWy36GEQ=,iv:uWzSspjA0emXdg3+FtkdTGVVLUi9FtJ5PAM8LsukM0I=,tag:qGYdst9pki1n4vZzbfXmrA==,type:str] +contexts: + - context: + cluster: ENC[AES256_GCM,data:NXfhLeqU4J0pbL6RV+oKjcISaUddhg0=,iv:bSch9N8ptQ7Io/1G++HH0UxRqaeT5Xnzru0e8wXjpJk=,tag:xy+VV7Ff7fzZxVIT/err/w==,type:str] + user: ENC[AES256_GCM,data:ySwATaTIWt/wbu8A7kBFpdwQzgD1j/s=,iv:FBA4/px2whl7gcotXa82zWsvVc1lN9A9XH9Z3VWDPvo=,tag:I7MqS9KNDGLH2534C4OmMQ==,type:str] + name: ENC[AES256_GCM,data:4dMxibxCOjlyqct9So24bcxuzImrYCI=,iv:P/Fp8/Zofybjcdm3s2hsUP4PqkiBPMIxVGb9jHvIiY4=,tag:/eTZxiy56Pv31J1TldnfLw==,type:str] +current-context: ENC[AES256_GCM,data:BsvfN8X4lWF+w/WUiDzCmJ9zj3fefOk=,iv:35osaNc0egPZda6KImdSSoRAui6oIQHIaHTEktWgkqI=,tag:s0fVURcej7Hr8pz32udMZA==,type:str] +kind: ENC[AES256_GCM,data:dbi0U3/R,iv:HXH+Dlv5wLI7Hfklkm1EHzG6xr/zyDVYzS2uPqGO4DU=,tag:OCIucU9fMKxUKQqfoqI8wg==,type:str] +preferences: {} +users: + - name: ENC[AES256_GCM,data:DlseQ4eDb9x/dOjO4BccHS9BPRA9mlo=,iv:jncX4keun8alirCaRDJ+PAkBf7BCsDBFYATYBPxG5O4=,tag:l42QMOEdaWX3l6CZVkm93Q==,type:str] + user: + client-certificate-data: ENC[AES256_GCM,data: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,iv:onvDUevuQrPu9iZE3rhxDlNFczcekvzA9l+FUZXl6Uk=,tag:pUyO6w73kIpRMAWgUnReAQ==,type:str] + client-key-data: ENC[AES256_GCM,data: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,iv:6QCYm/Zq7nLQ3WZArqN7o7QjI6hgb5EusJscswhE214=,tag:PR/+r7Wiq5CXxksDyOmTYA==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: "2021-05-04T21:58:02Z" + enc: CiQA4OM7ePq/gEbeVzyyz49K389+FaatQjjRZeMK1ybwrioWnFkSSQBy9hCYjrj24PexUYPKIw031vRcp2S6Uy0jYjCfAPyDHtzDwKbU/7ZzScV4FyHTC19AJkni/jAsQqe/EWaEk122HvrIQzzUxCg= + azure_kv: [] + hc_vault: [] + age: [] + lastmodified: "2021-05-04T21:58:04Z" + mac: ENC[AES256_GCM,data:nHbzDaorMMAL/1xBxftf/SUXiZAGZbj8HgfD+KOChoWV1W+njSa4GtOOfhftccf1Z6N0U2tSocEAYR+hNQc2zJrgQPUJrKg7pZHqk+R530kX1TL30AYtLDLdspnJ7JDuPv84DdR1DuFTzzkpdeK7lDtKH3etD6P2bHU9OeKAwOk=,iv:tgLaKzv9VFMeLEF1hE37Re1ZZA22qe+BzXq8jN0F5zA=,tag:97n6CR+YgpnPxdzZjXQgkw==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.1