diff --git a/charts/CHANGELOG b/charts/CHANGELOG new file mode 100644 index 00000000..4714a3d1 --- /dev/null +++ b/charts/CHANGELOG @@ -0,0 +1,188 @@ +0.8.18 +Fix ingress configuration snippet so that it is not bound by allow_django_admin + +0.8.17 +Feature to add NewRelic HTTP header and client-max-body-size to ingress + +0.8.16 +Updated the User ID use in k8s-cli-utils. It was updated in this PR (https://github.com/edx/k8s-cli-utils/pull/14) + +0.8.15 +Switch k8s-cli-utils image from dockerhub to ECR + +0.8.14 +Feature to allow configurable resource limits for CronJobs + +0.8.13 +Add feature to allow custom migration commands + +0.8.12 +Fix feature to allow worker resource limits to be configurable + +0.8.11 +Properly quote and escape shell script interpolations for deployments and cron jobs as well. + +0.8.10 +Add feature to allow worker resource limits to be configurable + +0.8.9 +Fix app deployment issue by adding the missing mounted volumes section when migrations are disabled + +0.8.8 +Proceed with running Django migrations only when it is enabled + +0.8.7 +Added additionalLabels env, owner and team for kubecost aggregations using range + +0.8.6 +Increase memory limit for celery workers to fix discovery celery workers from crashing due to resource limits being exceeded + +0.8.5 +Fix YAML injection prevention; `quote` output's escaping is only compatible with YAML, not Bash strings + +0.8.4 +Prevented YAML injection in worker command + +0.8.3 +Updated the Cron job template to support schedules that need to be quoted. + +0.8.2 +Updated the Cron job Api version to batch/v1. Previous api version was deprecated in k8s 1.21 and removed in 1.25. + +0.8.1 +Add option to configure the name of python (e.g. python, python3, python3.9) used to run command in migration init +container. Defaults to python3. + +0.8.0 +Removed ingress class variable in favor of new className variable. The class variable is used to set the old annotation +kubernetes.io/ingress.class which has been replaced by ingressClassName in the spec of the v1 of the ingress api + +0.7.2 +Add extra_tls_hosts variable to allow adding extra hostnames to TLS certs +Make ingress and tls secret names stable on ingresses using the new className variable to prevent shuffling. + +0.7.1 +Add new ingress class variable to fix issue with 0.7.0 that causes new ingresses to not be created, because kubernetes +doesn't allow you to set the new className variable on the spec and the old annotation at the same time. + +0.7.0 +Add ingressClassName to ingress spec for compatibility with networking.k8s.io/v1 Ingress and Kubernetes 1.22 + +0.6.2 +Add custom annotation to ingress object for external dns cloudflare + +0.6.1 +Adding option to specify command for collectstatic job + +0.6.0 +Changed imagePullPolicy to Always for app containers to fix issue with images not updating after rebuild. Currently +image tags are not immutable, so we need to always check the Docker image repository for updates images. + +0.5.8 +Add custom annotation for external dns + +0.5.7 +Add health_check.host_header to customize livenessProbe HTTP Host header + +0.5.6 +Add support for DB_MIGRATION_PASS with Bash special characters + +0.5.5 +Only create app HPA resource if app is enabled + +0.5.4 +Add option to toggle off app deployment (default set to True) + +0.5.3 +Upgrade ingress apiVersion from extensions/v1beta1 to networking.k8s.io/v1 + +0.5.2 +Adding option to specify resources for POD. + +0.5.1 +Adding option to specify initialDelaySeconds for readiness and liveness probes. + +0.5.0 +Removeing migrations from cronjobs and workers since this makes the db state harder to reason about. +Migrations will only be run when the application image is deployed. Remove migration secrets from cronjobs and workers since they +are no longer needed. + +0.4.1 +Removing mysql and elasticsearch subcharts + +0.4.0 +Removing support for development_mysql and development_elasticsearch + +0.3.8 +Default vault url updated to https://vault.prod.edx.org + +0.3.7 +Enabled tls by default for django-ida helm chart +added flag vault.use_tls to disable this behaviour. + +0.3.6 +New version of k8s-cli-util +Move from the older stable url to the newer one for the dev mysql deployment + +0.3.5 +New version of k8s-cli-util + +0.3.4 +Moved autoscaler api endpoints to use apps/v1 instead of apps/v1beta1, requires K8s > 1.10, but should +otherwise be reverse compatible. + +0.3.3 +Ingresses now have a generated number after them to prevent names colliding + +0.3.2 +Added parameter to allow the customization of the healthcheck endpoint with +/health as the default value. +health_check.endpoint: "/health" + +Added a liveness check that is different from the readiness check. + +0.3.1 +Added ability to override the app.port, default is backwards compatible + app.port: 18170 + +0.3.0 +Change defaults for the following variables as it was discovered +that the apps are mostly consistent, it is vault that is inconsistent. + app.migrations.migrate_db_user_env_name: DB_MIGRATION_USER + app.migrations.migrate_db_pass_env_name: DB_MIGRATION_PASS + +0.2.2 +Fix secret render indentation to fit configmap + +0.2.1 +Render config as Yaml instead of as a serialized map + +v0.2.0 +Added the following values to allow user to overwrite migration env names, +since they differ between applications. The following defaults were assigned: + + app.migrations.migrate_db_user_env_name: DATABASE_MIGRATE_USER + app.migrations.migrate_db_pass_env_name: DATABASE_MIGRATE_PASSWORD + +This is a breaking change since the default migrate_db_pass_env_name was +previously: DB_MIGRATION_PASS + +v0.1.1 +Fixed bug where image:tag pairings were not valid + +v0.1.0 +Added overridable issuer for ingresses for the cert issuer. +You will need to add an 'issuer' to each ingress using this version. + +v0.0.4 +Fix bug that resulted in an impossible autoscaling configuration min > max + +v0.0.3 +Allow applications to not specify a role_arn. Removed fake role ARN from service accounts by default. + +v0.0.2 +Added support for arbitrary application environment variables that get passed into all containers running the application image +to support applications that have non standard ENV setups. + +v0.0.1 +Initial commit diff --git a/charts/Chart.lock b/charts/Chart.lock new file mode 100644 index 00000000..b009d458 --- /dev/null +++ b/charts/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: django-ida + repository: https://25c38c15078aaa07ab0119be78db03b720b5e014@raw.githubusercontent.com/edx/helm-repo/master/ + version: 0.8.16 +digest: sha256:0e92ca10d7d40f4c92e8515b0db1c0ba1c6bcc2a43a5863bcbbf7ff75cab9679 +generated: "2023-10-30T14:08:00.791802-04:00" diff --git a/charts/Chart.yaml b/charts/Chart.yaml new file mode 100644 index 00000000..099604e8 --- /dev/null +++ b/charts/Chart.yaml @@ -0,0 +1,3 @@ +name: registrar +version: 0.0.1 +apiVersion: v2 diff --git a/charts/charts/django-ida-0.8.16.tgz b/charts/charts/django-ida-0.8.16.tgz new file mode 100644 index 00000000..c36d0b04 Binary files /dev/null and b/charts/charts/django-ida-0.8.16.tgz differ diff --git a/charts/requirements.lock b/charts/requirements.lock new file mode 100644 index 00000000..e69de29b diff --git a/charts/requirements.yaml b/charts/requirements.yaml new file mode 100644 index 00000000..e69de29b diff --git a/charts/templates/app-deployment.yaml b/charts/templates/app-deployment.yaml new file mode 100644 index 00000000..20303805 --- /dev/null +++ b/charts/templates/app-deployment.yaml @@ -0,0 +1,218 @@ +{{ if .Values.app.enabled}} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + name: {{ .Values.app.name }} + annotations: + ignore-check.kube-linter.io/no-read-only-root-fs: "Temporarily ignore check no-read-only-root-fs until PSRE-2074 is resolved" +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + template: + metadata: + labels: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + containers: + - args: + - "source /vault-api-secrets/secrets/secret.env && \ + exec {{ if .Values.newrelic.enabled }}newrelic-admin run-program{{ end }} \ + {{ regexReplaceAll "^\"|\"$" (quote .Values.app.command) "" }}" + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: NEW_RELIC_APP_NAME + valueFrom: + configMapKeyRef: + key: NEW_RELIC_APP_NAME + name: app-cm + - name: NEW_RELIC_LOG_LEVEL + valueFrom: + configMapKeyRef: + key: NEW_RELIC_LOG_LEVEL + name: app-cm + - name: NEW_RELIC_LICENSE_KEY + value: Secret value filled in from secrets.env + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} + imagePullPolicy: Always + livenessProbe: + initialDelaySeconds: {{ .Values.app.health_check.liveness_probe_initial_delay_seconds}} + exec: + command: + - ls + periodSeconds: 5 + name: {{ .Values.app.name }} + ports: + - containerPort: {{ .Values.app.port }} + name: http + protocol: TCP + readinessProbe: + initialDelaySeconds: {{ .Values.app.health_check.readiness_probe_initial_delay_seconds}} + httpGet: + httpHeaders: + - name: Host + value: {{ .Values.app.health_check.host_header }} + path: {{ .Values.app.health_check.endpoint }} + port: http + timeoutSeconds: 3 + resources: + limits: + cpu: {{ .Values.resources.limits.cpu }} + memory: {{ .Values.resources.limits.memory }} + requests: + cpu: {{ .Values.resources.requests.cpu }} + memory: {{ .Values.resources.requests.memory }} + volumeMounts: + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + initContainers: + - env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + - name: VAULT_ROLE + valueFrom: + configMapKeyRef: + key: VAULT_ROLE + name: app-cm + - name: TOKEN_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-token + - name: ACCESSOR_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-accessor + image: edxops/vault-kubernetes-authenticator:3b373bc86ade783492b6619552d2b172a6e12a8b + imagePullPolicy: IfNotPresent + name: vault-authenticator + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + resources: + requests: + cpu: "50m" + memory: "48Mi" + limits: + cpu: "100m" + memory: "64Mi" + securityContext: + readOnlyRootFilesystem: true + - command: + - /bin/sh + - -c + - | + set -xe + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/{{ .Values.app.secret_file_name }}:/vault-api-secrets/secrets/{{ .Values.app.secret_file_name }}" -once + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/secret.env:/vault-api-secrets/secrets/secret.env" -once + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/migrate.env:/vault-migrate-secrets/secrets/migrate.env" -once + env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + image: hashicorp/consul-template:0.20.0-light + imagePullPolicy: IfNotPresent + name: secret-render + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + readOnly: true + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + - mountPath: /vault-migrate-secrets/secrets + name: vault-migrate-secrets + - mountPath: /app-cm + name: app-cm + resources: + requests: + cpu: "50m" + memory: "48Mi" + limits: + cpu: "100m" + memory: "64Mi" + securityContext: + readOnlyRootFilesystem: true + {{ if .Values.app.migrations.enabled }} + - args: + - source /vault-migrate-secrets/secrets/migrate.env && {{ .Values.app.migrations.migration_command }} + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: DB_MIGRATION_USER + valueFrom: + configMapKeyRef: + key: {{ .Values.app.migrations.migrate_db_user_env_name }} + name: app-cm + optional: true + - name: {{ .Values.app.migrations.migrate_db_pass_env_name }} + value: Secret value filled in from migrate.env + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} + name: {{ .Values.app.migrations.name}} + resources: + limits: + cpu: 100m + memory: 512Mi + requests: + cpu: 25m + memory: 512Mi + volumeMounts: + - mountPath: /vault-migrate-secrets/secrets + name: vault-migrate-secrets + readOnly: true + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + {{ end }} + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + serviceAccountName: {{ .Values.app.service_account_name }} + volumes: + - emptyDir: + medium: Memory + name: vault-auth-secrets + - emptyDir: + medium: Memory + name: vault-api-secrets + - emptyDir: + medium: Memory + name: vault-migrate-secrets + - configMap: + name: app-cm + name: app-cm + +{{ end }} diff --git a/charts/templates/app-hpa.yaml b/charts/templates/app-hpa.yaml new file mode 100644 index 00000000..ee4803b3 --- /dev/null +++ b/charts/templates/app-hpa.yaml @@ -0,0 +1,24 @@ + + +{{ if and .Values.app.enabled .Values.app.autoscaling.enabled}} +--- +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + name: {{ .Values.app.name }} +spec: + minReplicas: {{ .Values.app.autoscaling.minReplicas }} + maxReplicas: {{ .Values.app.autoscaling.maxReplicas }} + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Values.app.name }} + targetCPUUtilizationPercentage: {{ .Values.app.autoscaling.targetCPUUtilizationPercentage }} + +{{ end }} diff --git a/charts/templates/app-ingress.yaml b/charts/templates/app-ingress.yaml new file mode 100644 index 00000000..124eb385 --- /dev/null +++ b/charts/templates/app-ingress.yaml @@ -0,0 +1,68 @@ +{{- range $index, $ingress := .Values.ingresses }} +{{- with $ }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: {{ $ingress.issuer }} + kubernetes.io/tls-acme: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + {{- if $ingress.external_dns }} + kubernetes.io/external-dns: "true" + {{- end }} + {{- if $ingress.external_dns_cloudflare }} + kubernetes.io/external-dns-cloudflare: "true" + {{- end }} + {{- if hasKey $ingress "client-max-body-size" }} + nginx.ingress.kubernetes.io/proxy-body-size: {{ index $ingress "client-max-body-size" }} + {{- end }} + nginx.ingress.kubernetes.io/configuration-snippet: |- + {{- if .Values.newrelic.enabled }} + proxy_set_header X-Queue-Start "t=${msec}"; + {{- end }} + {{- if $ingress.allow_django_admin }} + {{- else }} + server_tokens off; + location /admin { + deny all; + return 403; + } + {{- end }} + labels: + app.kubernetes.io/instance: {{ $.Values.app.name }} + app.kubernetes.io/name: {{ $.Values.app.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + {{- /* + Append truncated sha256 of hostname in case we have mutliple ingresses of the same className + */}} + name: {{ $.Values.app.name }}-{{ $ingress.className }}-{{ $ingress.host | sha256sum | trunc 5 }} +spec: + ingressClassName: {{ $ingress.className }} + rules: + - host: {{ $ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $.Values.app.name }} + port: + name: http + tls: + - hosts: + - {{ $ingress.host }} + {{- if $ingress.extra_tls_hosts }} + {{- range $ingress.extra_tls_hosts }} + - {{ . }} + {{- end }} + {{- end }} + {{- /* + Append truncated sha256 of hostname in case we have mutliple ingresses of the same className + */}} + secretName: {{ $.Values.app.name }}-tls-{{ $ingress.className }}-{{ $ingress.host | sha256sum | trunc 5 }} +{{- end }} +{{- end }} diff --git a/charts/templates/app-sa.yaml b/charts/templates/app-sa.yaml new file mode 100644 index 00000000..9d17152d --- /dev/null +++ b/charts/templates/app-sa.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + {{- if .Values.app.role_arn }} + annotations: + eks.amazonaws.com/role-arn: {{ .Values.app.role_arn }} + {{- end }} + name: {{ .Values.app.service_account_name }} diff --git a/charts/templates/app-service.yaml b/charts/templates/app-service.yaml new file mode 100644 index 00000000..517867e6 --- /dev/null +++ b/charts/templates/app-service.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + name: {{ .Values.app.name }} +spec: + ports: + - name: http + port: {{ .Values.app.port }} + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: {{ .Values.app.name }} + app.kubernetes.io/name: {{ .Values.app.name }} + type: ClusterIP diff --git a/charts/templates/collectstatic-deployment.yaml b/charts/templates/collectstatic-deployment.yaml new file mode 100644 index 00000000..1e4aa1d8 --- /dev/null +++ b/charts/templates/collectstatic-deployment.yaml @@ -0,0 +1,194 @@ + +{{ if .Values.collectstatic.enabled }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: {{ .Values.collectstatic.job_name }} + app.kubernetes.io/name: {{ .Values.collectstatic.job_name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + name: {{ .Values.collectstatic.job_name }} +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + app.kubernetes.io/instance: {{ .Values.collectstatic.job_name }} + app.kubernetes.io/name: {{ .Values.collectstatic.job_name }} + template: + metadata: + labels: + app.kubernetes.io/instance: {{ .Values.collectstatic.job_name }} + app.kubernetes.io/name: {{ .Values.collectstatic.job_name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + containers: + - args: + - echo asset upload complete;date; while true; do sleep 999999; done; + command: + - /bin/sh + - -c + - -- + image: busybox:latest + imagePullPolicy: IfNotPresent + name: print-completion-time + resources: + limits: + cpu: 5m + memory: 500Mi + requests: + cpu: 5m + memory: 100Mi + initContainers: + - env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + - name: VAULT_ROLE + valueFrom: + configMapKeyRef: + key: VAULT_ROLE + name: app-cm + - name: TOKEN_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-token + - name: ACCESSOR_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-accessor + image: edxops/vault-kubernetes-authenticator:latest + imagePullPolicy: IfNotPresent + name: vault-authenticator + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + - command: + - /bin/sh + - -c + - | + set -xe + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/{{ .Values.app.secret_file_name }}:/vault-api-secrets/secrets/{{ .Values.app.secret_file_name }}" -once + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/secret.env:/vault-api-secrets/secrets/secret.env" -once + env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + image: hashicorp/consul-template:0.20.0-light + imagePullPolicy: IfNotPresent + name: secret-render + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + readOnly: true + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + - mountPath: /app-cm + name: app-cm + - args: + - source /vault-api-secrets/secrets/secret.env && {{ .Values.collectstatic.command }} + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: NEW_RELIC_APP_NAME + valueFrom: + configMapKeyRef: + key: NEW_RELIC_APP_NAME + name: app-cm + - name: NEW_RELIC_LOG_LEVEL + valueFrom: + configMapKeyRef: + key: NEW_RELIC_LOG_LEVEL + name: app-cm + - name: NEW_RELIC_LICENSE_KEY + value: Secret value filled in from secrets.env + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} + imagePullPolicy: Always + name: run-collectstatic + resources: + limits: + cpu: 5m + memory: 500Mi + requests: + cpu: 5m + memory: 100Mi + volumeMounts: + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + - mountPath: /tmp/static + name: app-static + readOnly: false + - args: + - '[[ -n "${S3_ASSET_BUCKET}" ]] && aws s3 sync /tmp/static $S3_ASSET_BUCKET' + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: NEW_RELIC_APP_NAME + valueFrom: + configMapKeyRef: + key: NEW_RELIC_APP_NAME + name: app-cm + - name: NEW_RELIC_LOG_LEVEL + valueFrom: + configMapKeyRef: + key: NEW_RELIC_LOG_LEVEL + name: app-cm + - name: NEW_RELIC_LICENSE_KEY + value: Secret value filled in from secrets.env + - name: S3_ASSET_BUCKET + valueFrom: + configMapKeyRef: + key: S3_ASSET_BUCKET + name: app-cm + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: 257477529851.dkr.ecr.us-east-1.amazonaws.com/k8s-cli-utils:latest + imagePullPolicy: IfNotPresent + name: s3-upload + volumeMounts: + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + - mountPath: /tmp/static + name: app-static + readOnly: true + securityContext: + fsGroup: 999 + runAsGroup: 999 + runAsUser: 999 + serviceAccountName: {{ .Values.collectstatic.job_name }} + volumes: + - emptyDir: + medium: Memory + name: vault-auth-secrets + - emptyDir: + medium: Memory + name: vault-api-secrets + - configMap: + name: app-cm + name: app-cm + - emptyDir: + medium: Memory + name: app-static +{{ end }} diff --git a/charts/templates/collectstatic-sa.yaml b/charts/templates/collectstatic-sa.yaml new file mode 100644 index 00000000..e92a5e9c --- /dev/null +++ b/charts/templates/collectstatic-sa.yaml @@ -0,0 +1,9 @@ +{{ if .Values.collectstatic.enabled }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + eks.amazonaws.com/role-arn: {{ .Values.collectstatic.asset_write_role }} + name: {{ .Values.collectstatic.job_name }} +{{ end }} diff --git a/charts/templates/configmap.yaml b/charts/templates/configmap.yaml new file mode 100644 index 00000000..970d1659 --- /dev/null +++ b/charts/templates/configmap.yaml @@ -0,0 +1,47 @@ +--- +kind: ConfigMap +metadata: + annotations: {} + labels: {} + name: app-cm +apiVersion: v1 +data: + {{- if .Values.app.migrations.enabled }} + {{ .Values.app.migrations.migrate_db_user_env_name }}: {{ .Values.app.migrations.database_migrate_user }} + {{- end }} + NEW_RELIC_APP_NAME: {{ .Values.newrelic.app_name }} + NEW_RELIC_LOG_LEVEL: {{ .Values.newrelic.log_level }} + S3_ASSET_BUCKET: {{ .Values.collectstatic.s3_bucket }} + VAULT_ADDR: {{ .Values.vault.vault_addr }} + VAULT_ROLE: {{ .Values.vault.vault_role }} + {{ .Values.app.secret_file_name }}: | + --- + {{"{{"}} with secret "{{ .Values.vault.secret_name }}?version={{ .Values.vault.secret_version }}" {{"}}"}} +{{ toYaml .Values.app.config | indent 4}} + {{"{{"}} end {{"}}"}} + migrate.env: |+ + #!/bin/bash + {{"{{"}} with secret "{{ .Values.vault.secret_name }}?version={{ .Values.vault.secret_version }}" {{"}}"}} + export {{ .Values.app.migrations.migrate_db_pass_env_name }}={{"$'{{"}} .Data.data.{{ .Values.app.migrations.migrate_db_pass_env_name }} | replaceAll "\\" "\\\\" | replaceAll "'" "\\'" {{"}}'"}} + {{"{{"}} end {{"}}"}} + + secret.env: | + #!/bin/bash + export NEW_RELIC_LICENSE_KEY={{"{{"}} with secret "{{ .Values.vault.secret_name }}?version={{ .Values.vault.secret_version }}" {{"}}"}}{{"{{"}} .Data.data.NEW_RELIC_LICENSE_KEY {{"}}"}}{{"{{"}} end {{"}}"}} +{{ if .Values.vault.use_tls }} + vault.hcl: | + "vault" = { + "vault_agent_token_file" = "/vault-auth-secrets/secrets/.vault-token" + ssl { + enabled = true + verify = true + ca_cert = "/etc/ssl/cert.pem" + } + } +{{ else }} + vault.hcl: | + "vault" = { + "vault_agent_token_file" = "/vault-auth-secrets/secrets/.vault-token" + } +{{ end }} + diff --git a/charts/templates/cronjobs.yaml b/charts/templates/cronjobs.yaml new file mode 100644 index 00000000..63ad0620 --- /dev/null +++ b/charts/templates/cronjobs.yaml @@ -0,0 +1,121 @@ +{{- range .Values.cronjobs }} +{{- $job := . -}} +{{- with $ }} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ $job.name }} +spec: + concurrencyPolicy: Forbid + schedule: "{{ $job.schedule }}" + jobTemplate: + spec: + template: + spec: + containers: + - args: + - "source /vault-api-secrets/secrets/secret.env && \ + exec {{ if $.Values.newrelic.enabled }}newrelic-admin run-program{{ end }} \ + {{ regexReplaceAll "^\"|\"$" (quote $job.command) "" }}" + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: NEW_RELIC_APP_NAME + valueFrom: + configMapKeyRef: + key: NEW_RELIC_APP_NAME + name: app-cm + - name: NEW_RELIC_LOG_LEVEL + valueFrom: + configMapKeyRef: + key: NEW_RELIC_LOG_LEVEL + name: app-cm + - name: NEW_RELIC_LICENSE_KEY + value: Secret value filled in from secrets.env + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} + imagePullPolicy: Always + name: {{ $job.name }} + resources: + limits: + cpu: {{ (($job.resources).limits).cpu | default "100m" }} + memory: {{ (($job.resources).limits).memory | default "1Gi" }} + requests: + cpu: {{ (($job.resources).requests).cpu | default "25m" }} + memory: {{ (($job.resources).requests).memory | default "512Mi" }} + volumeMounts: + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + initContainers: + - env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + - name: VAULT_ROLE + valueFrom: + configMapKeyRef: + key: VAULT_ROLE + name: app-cm + - name: TOKEN_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-token + - name: ACCESSOR_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-accessor + image: edxops/vault-kubernetes-authenticator:latest + imagePullPolicy: IfNotPresent + name: vault-authenticator + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + - command: + - /bin/sh + - -c + - | + set -xe + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/{{ .Values.app.secret_file_name }}:/vault-api-secrets/secrets/{{ .Values.app.secret_file_name }}" -once + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/secret.env:/vault-api-secrets/secrets/secret.env" -once + env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + image: hashicorp/consul-template:0.20.0-light + imagePullPolicy: IfNotPresent + name: secret-render + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + readOnly: true + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + - mountPath: /app-cm + name: app-cm + restartPolicy: Never + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + serviceAccountName: {{ .Values.app.service_account_name }} + volumes: + - emptyDir: + medium: Memory + name: vault-auth-secrets + - emptyDir: + medium: Memory + name: vault-api-secrets + - configMap: + name: app-cm + name: app-cm +{{- end }} +{{- end }} diff --git a/charts/templates/workers.yaml b/charts/templates/workers.yaml new file mode 100644 index 00000000..62c283d3 --- /dev/null +++ b/charts/templates/workers.yaml @@ -0,0 +1,158 @@ +{{- range .Values.workers }} +{{- $worker := . -}} +{{- with $ }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: {{ $worker.name }} + app.kubernetes.io/name: {{ $worker.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + name: {{ $worker.name }} +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + app.kubernetes.io/instance: {{ $worker.name }} + app.kubernetes.io/name: {{ $worker.name }} + template: + metadata: + labels: + app.kubernetes.io/instance: {{ $worker.name }} + app.kubernetes.io/name: {{ $worker.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + containers: + - args: + - "source /vault-api-secrets/secrets/secret.env && \ + exec {{ if $.Values.newrelic.enabled }}newrelic-admin run-program{{ end }} \ + {{ regexReplaceAll "^\"|\"$" (quote $worker.command) "" }}" + command: + - /bin/bash + - -c + - -- + env: + - name: {{ .Values.app.secret_file_env_name }} + value: /vault-api-secrets/secrets/{{ .Values.app.secret_file_name }} + - name: NEW_RELIC_APP_NAME + valueFrom: + configMapKeyRef: + key: NEW_RELIC_APP_NAME + name: app-cm + - name: NEW_RELIC_LOG_LEVEL + valueFrom: + configMapKeyRef: + key: NEW_RELIC_LOG_LEVEL + name: app-cm + - name: NEW_RELIC_LICENSE_KEY + value: Secret value filled in from secrets.env + {{- range $env := .Values.app.extraEnvs }} + - name: {{ $env.name }} + value: {{ $env.value }} + {{- end }} + image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} + imagePullPolicy: Always + name: {{ $worker.name }} + resources: + limits: + cpu: {{ (($worker.resources).limits).cpu | default "125m" }} + memory: {{ (($worker.resources).limits).memory | default "2Gi" }} + requests: + cpu: {{ (($worker.resources).requests).cpu | default "25m" }} + memory: {{ (($worker.resources).requests).memory | default "512Mi" }} + volumeMounts: + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + readOnly: true + initContainers: + - env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + - name: VAULT_ROLE + valueFrom: + configMapKeyRef: + key: VAULT_ROLE + name: app-cm + - name: TOKEN_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-token + - name: ACCESSOR_DEST_PATH + value: /vault-auth-secrets/secrets/.vault-accessor + image: edxops/vault-kubernetes-authenticator:latest + imagePullPolicy: IfNotPresent + name: vault-authenticator + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + - command: + - /bin/sh + - -c + - | + set -xe + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/{{ .Values.app.secret_file_name }}:/vault-api-secrets/secrets/{{ .Values.app.secret_file_name }}" -once + /bin/consul-template -config /app-cm/vault.hcl -template "/app-cm/secret.env:/vault-api-secrets/secrets/secret.env" -once + env: + - name: VAULT_ADDR + valueFrom: + configMapKeyRef: + key: VAULT_ADDR + name: app-cm + image: hashicorp/consul-template:0.20.0-light + imagePullPolicy: IfNotPresent + name: secret-render + volumeMounts: + - mountPath: /vault-auth-secrets/secrets + name: vault-auth-secrets + readOnly: true + - mountPath: /vault-api-secrets/secrets + name: vault-api-secrets + - mountPath: /app-cm + name: app-cm + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + serviceAccountName: {{ .Values.app.service_account_name }} + volumes: + - emptyDir: + medium: Memory + name: vault-auth-secrets + - emptyDir: + medium: Memory + name: vault-api-secrets + - configMap: + name: app-cm + name: app-cm + +{{ if .Values.app.autoscaling.enabled}} +--- +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: {{ $worker.name }} + app.kubernetes.io/name: {{ $worker.name }} + {{- range $key, $val := .Values.app.additionalLabels }} + {{ $key }}: {{ $val }} + {{- end}} + name: {{ $worker.name }} +spec: + minReplicas: {{ $worker.minReplicas }} + maxReplicas: {{ $worker.maxReplicas }} + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ $worker.name }} + targetCPUUtilizationPercentage: {{ $worker.targetCPUUtilizationPercentage }} + {{ end }} + {{- end }} + {{- end }} diff --git a/charts/values.yaml b/charts/values.yaml new file mode 100644 index 00000000..35f1cec7 --- /dev/null +++ b/charts/values.yaml @@ -0,0 +1,130 @@ +registrar: + app: + name: registrar + role_arn: arn:aws:iam::708756755355:role/development-edx-registrar + command: 'gunicorn --workers=2 --name registrar -c /edx/app/registrar/registrar/docker_gunicorn_configuration.py --log-file - --max-requests=1000 registrar.wsgi:application' + + port: 18734 + + secret_file_env_name: REGISTRAR_CFG + secret_file_name: registrar.yml + service_account_name: registrar + migrations: + name: registrar-migrations + enabled: true + database_migrate_user: db-user + + autoscaling: + enabled: false + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 50 + + health_check: + liveness_probe_initial_delay_seconds: 30 + readiness_probe_initial_delay_seconds: 30 + + # FILL-ME-IN + config: + API_ROOT: https://api.development.edx.org/registrar + BACKEND_SERVICE_EDX_OAUTH2_KEY: '{{ .Data.data.BACKEND_SERVICE_EDX_OAUTH2_KEY }}' + BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL: https://courses.development.edx.org/oauth2 + BACKEND_SERVICE_EDX_OAUTH2_SECRET: '{{ .Data.data.BACKEND_SERVICE_EDX_OAUTH2_SECRET}}' + CACHES: + default: + BACKEND: django.core.cache.backends.memcached.MemcachedCache + KEY_PREFIX: registrar + LOCATION: + - development-edx-registrar.6sxrym.0001.use1.cache.amazonaws.com + - development-edx-registrar.6sxrym.0002.use1.cache.amazonaws.com + CELERY_ALWAYS_EAGER: false + CELERY_BROKER_HOSTNAME: edx-development-queues.6sxrym.ng.0001.use1.cache.amazonaws.com:6379 + CELERY_BROKER_PASSWORD: '' + CELERY_BROKER_TRANSPORT: redis + CELERY_BROKER_USER: '' + CELERY_BROKER_VHOST: 0 + CELERY_DEFAULT_EXCHANGE: registrar + CELERY_DEFAULT_QUEUE: registrar.default + CELERY_DEFAULT_ROUTING_KEY: registrar + CORS_ORIGIN_WHITELIST: + - https://development-edx-registrar.edx.org + - https://registrar.development.edx.org + - https://program-console.development.edx.org + CSRF_COOKIE_SECURE: true + CSRF_TRUSTED_ORIGINS: + - .edx.org + CSRF_TRUSTED_ORIGINS_WITH_SCHEMES: + - https://*.edx.org + DATABASES: + default: + ATOMIC_REQUESTS: false + CONN_MAX_AGE: 60 + ENGINE: django.db.backends.mysql + HOST: mysql.mysql + NAME: db + OPTIONS: + connect_timeout: 10 + init_command: SET sql_mode='STRICT_TRANS_TABLES' + PASSWORD: '{{ .Data.data.DATABASE_DEFAULT_PASSWORD }}' + PORT: '3306' + USER: db-user + DISCOVERY_BASE_URL: https://discovery.development.edx.org + EDX_DRF_EXTENSIONS: + OAUTH2_USER_INFO_URL: https://courses.development.edx.org/oauth2/user_info + JWT_AUTH: + JWT_AUTH_COOKIE_HEADER_PAYLOAD: development-edx-jwt-cookie-header-payload + JWT_AUTH_COOKIE_SIGNATURE: development-edx-jwt-cookie-signature + JWT_ISSUERS: + - AUDIENCE: '{{ .Data.data.JWT_ISSUERS_0_AUDIENCE }}' + ISSUER: https://courses.development.edx.org/oauth2 + SECRET_KEY: '{{ .Data.data.JWT_ISSUERS_0_SECRET_KEY }}' + JWT_PUBLIC_SIGNING_JWK_SET: '{"keys": [{"n": "hcm7899L5XQ6AVNYwNo3Yu-rx47f0FMAN3am6WgurbDulrcCIfhyTivzpnuOY0W-2tntlR51j4hHzywSSCqdOgG1MZLfVSJwVpVUhd9ROLuIRbifXyRJ1_d7C_L3YZdyYqFY7k8W5f62UqCePxVCh-zCKtkfjCJkhRujgDw4YeL63j80We48T0LYK5ZSRBOEj2N4fjbzsi9T2d1qCBaLvXwgYzMnUTc8mch6JMP8HWsrgqV4kkPyP3il_IgRARV5BF5cdJbUg2-__5QirmLF16xl9j0vo9yLyBnqlYZXWYjFOECI7FatHLGQDT5TopXWT4YF82_aZSNuIQUoDY8hDQ", + "kty": "RSA", "e": "AQAB", "kid": "lmsdevelopment002"}]}' + LMS_BASE_URL: https://courses.development.edx.org + MEDIA_STORAGE_BACKEND: + AWS_DEFAULT_ACL: null + AWS_LOCATION: '' + AWS_QUERYSTRING_AUTH: true + AWS_QUERYSTRING_EXPIRE: 3600 + DEFAULT_FILE_STORAGE: storages.backends.s3boto3.S3Boto3Storage + REGISTRAR_BUCKET: development-edx-registrar + PROGRAM_REPORTS_BUCKET: development-edx-program-reports + REGISTRAR_SERVICE_USER: registrar_service_user + SECRET_KEY: '{{ .Data.data.SECRET_KEY }}' + SEGMENT_KEY: '{{ .Data.data.SEGMENT_KEY }}' + SESSION_COOKIE_SECURE: true + SOCIAL_AUTH_EDX_OAUTH2_ISSUER: https://courses.development.edx.org + SOCIAL_AUTH_EDX_OAUTH2_KEY: '{{ .Data.data.SOCIAL_AUTH_EDX_OAUTH2_KEY }}' + SOCIAL_AUTH_EDX_OAUTH2_LOGOUT_URL: https://courses.development.edx.org/logout + SOCIAL_AUTH_EDX_OAUTH2_SECRET: '{{ .Data.data.SOCIAL_AUTH_EDX_OAUTH2_SECRET }}' + SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT: https://courses.development.edx.org + STATIC_ROOT: /tmp/static + + workers: [] + # - name: registrar-worker + # command: celery -A registrar worker --loglevel info + # minReplicas: 3 + # maxReplicas: 6 + # targetCPUUtilizationPercentage: 100 + + newrelic: + enabled: false + app_name: development-edx-registrar + log_level: info + + collectstatic: + enabled: false + + vault: + enabled: true + vault_role: registrar + vault_addr: http://development-vault.vault:8200 + secret_name: kv/registrar + secret_version: 1 + + ingresses: + - host: registrar-eks.development.edx.org + class: nginx + issuer: selfsigning-issuer + + cronjobs: [] diff --git a/devspace.yaml b/devspace.yaml new file mode 100644 index 00000000..5d09341a --- /dev/null +++ b/devspace.yaml @@ -0,0 +1,98 @@ +version: v2beta1 +name: registrargit + +# This is a list of `pipelines` that DevSpace can execute (you can define your own) +pipelines: + # This is the pipeline for the main command: `devspace dev` (or `devspace run-pipeline dev`) + dev: + run: |- + run_dependencies --all # 1. Deploy any projects this project needs (see "dependencies") + ensure_pull_secrets --all # 2. Ensure pull secrets + create_deployments --all # 3. Deploy Helm charts and manifests specfied as "deployments" + start_dev app # 4. Start dev mode "app" (see "dev" section) + # You can run this pipeline via `devspace deploy` (or `devspace run-pipeline deploy`) + deploy: + run: |- + run_dependencies --all # 1. Deploy any projects this project needs (see "dependencies") + ensure_pull_secrets --all # 2. Ensure pull secrets + build_images --all -t $(git describe --always) # 3. Build, tag (git commit hash) and push all images (see "images") + create_deployments --all # 4. Deploy Helm charts and manifests specfied as "deployments" + +# This is a list of `images` that DevSpace can build for this project +# We recommend to skip image building during development (devspace dev) as much as possible +images: + app: + image: edxops/registrar + dockerfile: ./Dockerfile + +# This is a list of `deployments` that DevSpace can create for this project +deployments: + app: + # This deployment uses `helm` but you can also define `kubectl` deployments or kustomizations + helm: + # We are deploying this project with the Helm chart you provided + chart: + name: .devspace/chart-repo/argocd/applications/registrar + # Under `values` we can define the values for this Helm chart used during `helm install/upgrade` + # You may also use `valuesFiles` to load values from files, e.g. valuesFiles: ["values.yaml"] + valuesFiles: + - ./charts/development-config.yaml + vault: + helm: + chart: + name: .devspace/chart-repo/argocd/applications/vault + valuesFiles: + - .devspace/chart-repo/argocd/applications/vault/development.yaml + + vault-bootstrapper: + kubectl: + kustomize: true + manifests: + - ./vault-development-bootstrapper/ + +# This is a list of `dev` containers that are based on the containers created by your deployments +dev: + app: + # Search for the container that runs this image + imageSelector: edxops/registrar + # Sync files between the local filesystem and the development container + sync: + - path: ./ + uploadExcludeFile: .dockerignore + # Open a terminal and use the following command to start it + terminal: + command: ./devspace_start.sh + # Inject a lightweight SSH server into the container (so your IDE can connect to the remote dev env) + ssh: + enabled: true + # Make the following commands from my local machine available inside the dev container + proxyCommands: + - command: devspace + - command: kubectl + - command: helm + - gitCredentials: true + # Forward the following ports to be able access your application via localhost + ports: + - port: "18734" + # Open the following URLs once they return an HTTP status code other than 502 or 503 + open: + - url: http://localhost:18734 + +# Use the `commands` section to define repeatable dev workflows for this project +commands: + migrate-db: + command: |- + echo 'This is a cross-platform, shared command that can be used to codify any kind of dev task.' + echo 'Anyone using this project can invoke it via "devspace run migrate-db"' +hooks: + - events: + - before:deploy + command: if [ -d '.devspace/chart-repo/.git' ]; then cd ".devspace/chart-repo" && git pull origin master; else mkdir -p .devspace/chart-repo; git clone --single-branch --branch master git@github.com:edx/edx-internal.git .devspace/chart-repo; fi + +# Define dependencies to other projects with a devspace.yaml +# dependencies: +# api: +# git: https://... # Git-based dependencies +# tag: v1.0.0 +# ui: +# path: ./ui # Path-based dependencies (for monorepos) diff --git a/devspace_start.sh b/devspace_start.sh new file mode 100755 index 00000000..d681506b --- /dev/null +++ b/devspace_start.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set +e # Continue on errors + +COLOR_BLUE="\033[0;94m" +COLOR_GREEN="\033[0;92m" +COLOR_RESET="\033[0m" + +# Print useful output for user +echo -e "${COLOR_BLUE} + %########% + %###########% ____ _____ + %#########% | _ \ ___ __ __ / ___/ ____ ____ ____ ___ + %#########% | | | | / _ \\\\\ \ / / \___ \ | _ \ / _ | / __// _ \\ + %#############% | |_| |( __/ \ V / ____) )| |_) )( (_| |( (__( __/ + %#############% |____/ \___| \_/ \____/ | __/ \__,_| \___\\\\\___| + %###############% |_| + %###########%${COLOR_RESET} + + +Welcome to your development container! + +This is how you can work with it: +- Files will be synchronized between your local machine and this container +- Some ports will be forwarded, so you can access this container via localhost +- Run \`${COLOR_GREEN}python main.py${COLOR_RESET}\` to start the application +" + +# Set terminal prompt +export PS1="\[${COLOR_BLUE}\]devspace\[${COLOR_RESET}\] ./\W \[${COLOR_BLUE}\]\\$\[${COLOR_RESET}\] " +if [ -z "$BASH" ]; then export PS1="$ "; fi + +# Include project's bin/ folder in PATH +export PATH="./bin:$PATH" + +# Open shell +bash --norc